diff --git a/.dockerignore b/.dockerignore index 076b1f5..b61e39c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -22,6 +22,8 @@ __pycache__ **/.ruff_cache .mypy_cache **/.mypy_cache +**/.cache +**/.feature-cache .coverage htmlcov node_modules @@ -41,8 +43,28 @@ storage/covers/* reports docs/PROJECT_RESEARCH_BRIEF.md apps/api/tests/test_hubert_alignment_real.py +apps/api/tests/test_chart_engine_real_data.py +apps/api/tests/test_chart_rhythm_policy.py dist build +wandb +runs +checkpoints +*.pt +*.pth +*.ckpt +*.safetensors +*.onnx +*.gguf +*.npy +*.npz +*.pkl +*.joblib +*.sm +*.ssc +*.beatforge.zip +*.rar +*.7z *.db *.db-* *.sqlite @@ -53,4 +75,6 @@ build *.m4a *.aac *.ogg +local-data +材料 *.log diff --git a/.env.example b/.env.example index bf68ea0..255bf61 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,10 @@ BEATFORGE_WEB_PORT=5173 BEATFORGE_DATABASE_URL=sqlite:///./storage/beatforge.db BEATFORGE_STORAGE_DIR=./storage BEATFORGE_MAX_UPLOAD_BYTES=262144000 +# Optional override for a licensed, local-only SPEED reference corpus: +# BEATFORGE_SPEED_CHARTS_DIR=./local-data/speed-corpus +# Optional private BeatForge package used only by local acceptance tests: +# BEATFORGE_REAL_CHART_PACKAGE=./local-data/reference-song.beatforge.zip BEATFORGE_DEMUCS_MODEL=htdemucs BEATFORGE_DEMUCS_DEVICE=auto BEATFORGE_QWEN_ASR_MODEL=./storage/models/Qwen3-ASR-1.7B diff --git a/.gitignore b/.gitignore index 61d7e6c..ca4b608 100644 --- a/.gitignore +++ b/.gitignore @@ -25,10 +25,32 @@ __pycache__/ .pytest_cache/ .ruff_cache/ .mypy_cache/ +**/.cache/ +**/.feature-cache/ .coverage htmlcov/ dist/ build/ +wandb/ +runs/ +checkpoints/ + +# Local model, dataset and export artifacts. +*.pt +*.pth +*.ckpt +*.safetensors +*.onnx +*.gguf +*.npy +*.npz +*.pkl +*.joblib +*.sm +*.ssc +*.beatforge.zip +*.rar +*.7z # JavaScript dependencies, caches and builds node_modules/ @@ -56,6 +78,10 @@ storage/covers/* *.aac *.ogg +# Local copyrighted reference corpora used by AI Chart Engine training/tests. +local-data/ +材料/ + # Public reports are limited to deterministic synthetic-demo measurements. reports/* !reports/demo-evaluation.json @@ -64,6 +90,8 @@ reports/* # Local-only acceptance material derived from private projects. docs/PROJECT_RESEARCH_BRIEF.md apps/api/tests/test_hubert_alignment_real.py +apps/api/tests/test_chart_engine_real_data.py +apps/api/tests/test_chart_rhythm_policy.py # Runtime files *.db diff --git a/README.md b/README.md index 2780b10..c94b0d8 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,24 @@ DAW 风格界面中。 - Canvas 时间轴缩放、定位、拖动、多选、锁定、吸附、Undo/Redo 与自动保存。 - 区分真实声学位置 `acousticSample` 与音游参考位置 `chartSample`,BPM 不覆盖声学证据。 - 一键导出含参考音频的 BeatForge 制谱包,兼容 JSON、CSV API。 +- 本地 AI Chart Engine:读取用户提供且有权使用的 SPEED 参考语料,以 1–15 难度生成、优化、验证并导出五轨 SM。 + +## AI Chart Engine + +`/chart-engine` 提供本地五轨参考谱面库和音频同步预览;项目中的“AI 制谱”工作区把当前歌曲的 +BeatForge 声学候选转换为五轨谱面,支持独立转圈开关、可玩性报告和 SM 导出。生成逻辑与 +可选 Transformer 均完全本地运行,checkpoint 不存在时使用从本地授权语料统计得到的确定性规则。 + +```bash +.venv/bin/python scripts/chart_engine.py inventory +.venv/bin/python scripts/chart_engine.py build-dataset \ + --mode pump-single --analysis-mode balanced --analyze-missing +.venv/bin/python scripts/train_chart_model.py \ + --epochs 12 --batch-size 8 --sequence-length 512 --device auto +``` + +本地语料约定、数据集契约、训练参数、接口和存储结构见 +[AI Chart Engine 文档](docs/AI_CHART_ENGINE.md)。 ## 导出数据包 @@ -164,6 +182,7 @@ storage/ ├── vocal-alignment/ 模型子进程临时目录 ├── waveform/ 多级波形缓存 ├── analyses/ 分析快照 +├── chart-engine/ 真实训练数据、模型 checkpoint 与生成谱面 └── beatforge.db SQLite 项目与编辑数据 ``` diff --git a/apps/api/beatforge_api/chart_engine/__init__.py b/apps/api/beatforge_api/chart_engine/__init__.py new file mode 100644 index 0000000..046d4a3 --- /dev/null +++ b/apps/api/beatforge_api/chart_engine/__init__.py @@ -0,0 +1,17 @@ +"""Local five-panel chart generation, validation, import, and export.""" + +from .generator import generate_chart +from .library import ReferenceLibrary +from .sm import export_sm, parse_sm +from .statistics import chart_statistics, corpus_statistics +from .validator import validate_chart + +__all__ = [ + "ReferenceLibrary", + "chart_statistics", + "corpus_statistics", + "export_sm", + "generate_chart", + "parse_sm", + "validate_chart", +] diff --git a/apps/api/beatforge_api/chart_engine/dataset.py b/apps/api/beatforge_api/chart_engine/dataset.py new file mode 100644 index 0000000..3bd77c4 --- /dev/null +++ b/apps/api/beatforge_api/chart_engine/dataset.py @@ -0,0 +1,228 @@ +from __future__ import annotations + +import hashlib +import json +import os +import shutil +import tempfile +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Literal + +from ..audio.io import AudioDecodeError +from ..audio.pipeline import analyze_audio +from ..media import prepare_analysis_source +from .library import ReferenceAsset, ReferenceLibrary +from .statistics import corpus_statistics + +AnalysisMode = Literal["recall", "balanced", "clean", "accurate"] + + +@dataclass(slots=True) +class DatasetBuildReport: + source_chart_count: int + completed: int = 0 + skipped: int = 0 + failed: int = 0 + analyzed_audio_count: int = 0 + reused_analysis_count: int = 0 + samples: list[str] = field(default_factory=list) + errors: list[dict[str, str]] = field(default_factory=list) + + def to_dict(self) -> dict[str, Any]: + return { + "sourceChartCount": self.source_chart_count, + "completed": self.completed, + "skipped": self.skipped, + "failed": self.failed, + "analyzedAudioCount": self.analyzed_audio_count, + "reusedAnalysisCount": self.reused_analysis_count, + "samples": self.samples, + "errors": self.errors, + } + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def _write_json(path: Path, value: Any) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + temporary = path.with_name(f".{path.name}.tmp") + temporary.write_text(json.dumps(value, ensure_ascii=False, indent=2), encoding="utf-8") + temporary.replace(path) + + +def _materialize_audio(source: Path, target: Path) -> None: + target.parent.mkdir(parents=True, exist_ok=True) + if target.exists() or target.is_symlink(): + target.unlink() + try: + os.link(source, target) + except OSError: + try: + target.symlink_to(source) + except OSError: + shutil.copy2(source, target) + + +def _split_for_hash(audio_hash: str) -> str: + bucket = int(audio_hash[:8], 16) % 100 + if bucket < 80: + return "train" + if bucket < 90: + return "validation" + return "test" + + +def _analysis_payload(asset: ReferenceAsset, mode: AnalysisMode) -> dict[str, Any]: + decode_backend = "libsndfile" + try: + result = analyze_audio(asset.audio_path, mode=mode, sensitivity=0.5) + except AudioDecodeError: + # Some corpus MP3s expose valid metadata but fail during a complete + # libsndfile read. Decode those exact source bytes through the same local + # ffmpeg recovery path used by uploaded BeatForge tracks. + with tempfile.TemporaryDirectory(prefix="beatforge-chart-decode-") as directory: + decoded, _ = prepare_analysis_source( + asset.audio_path, Path(directory), force_ffmpeg=True + ) + result = analyze_audio(decoded, mode=mode, sensitivity=0.5) + decode_backend = "ffmpeg_pcm_f32le" + payload = result.to_dict() + payload["schema_version"] = "beatforge.analysis.v1" + payload["source_audio"] = asset.audio_path.name + payload["source_decode_backend"] = decode_backend + return payload + + +def build_dataset( + library: ReferenceLibrary, + output_dir: str | Path, + *, + mode: Literal["pump-single", "pump-double"] = "pump-single", + analyze_missing: bool = False, + analysis_mode: AnalysisMode = "balanced", + limit: int | None = None, + only_ids: set[str] | None = None, +) -> DatasetBuildReport: + """Build only complete real `(audio, BeatForge, chart)` training triples. + + When analysis is absent, no placeholder is written. Callers must explicitly + opt into running the production local analyzer with ``analyze_missing``. + """ + + output = Path(output_dir).expanduser().resolve() + output.mkdir(parents=True, exist_ok=True) + cache_dir = output / ".feature-cache" + cache_dir.mkdir(parents=True, exist_ok=True) + assets = [asset for asset in library.assets() if library.chart(asset.id).mode == mode] + if only_ids is not None: + assets = [asset for asset in assets if asset.id in only_ids] + if limit is not None: + assets = assets[: max(0, limit)] + report = DatasetBuildReport(source_chart_count=len(assets)) + built_charts = [] + manifest_samples: list[dict[str, Any]] = [] + for asset in assets: + chart = library.chart(asset.id) + audio_hash = _sha256(asset.audio_path) + feature_path = cache_dir / f"{audio_hash}.{analysis_mode}.json" + try: + if feature_path.is_file(): + analysis = json.loads(feature_path.read_text(encoding="utf-8")) + # Caches created before decode provenance was added came only + # from successful direct libsndfile analyses. + analysis.setdefault("source_decode_backend", "libsndfile") + report.reused_analysis_count += 1 + elif analyze_missing: + analysis = _analysis_payload(asset, analysis_mode) + _write_json(feature_path, analysis) + report.analyzed_audio_count += 1 + else: + report.skipped += 1 + report.errors.append( + { + "chartId": asset.id, + "code": "BEATFORGE_ANALYSIS_MISSING", + "message": ( + "No real BeatForge analysis exists for this SPEED audio. " + "Re-run with analyze_missing enabled." + ), + } + ) + continue + sample_dir = output / asset.id + sample_dir.mkdir(parents=True, exist_ok=True) + _materialize_audio(asset.audio_path, sample_dir / "audio.mp3") + _write_json( + sample_dir / "beatforge.json", + { + "schemaVersion": "beatforge.chart-training-features.v1", + "audioSha256": audio_hash, + "analysisMode": analysis_mode, + "analysis": analysis, + }, + ) + _write_json( + sample_dir / "chart.json", + chart.model_dump(by_alias=True, mode="json"), + ) + metadata = { + "schemaVersion": "beatforge.chart-training-sample.v1", + "songId": asset.id, + "sourceGroup": asset.group, + "title": chart.title, + "mode": chart.mode, + "difficulty": chart.meter, + "chartBpm": chart.bpm, + "chartOffsetSec": chart.offset_sec, + "durationSec": chart.duration_sec, + "analysisBpm": analysis.get("bpm"), + "analysisBpmConfidence": analysis.get("bpm_confidence"), + "audioSha256": audio_hash, + "chartSha256": _sha256(asset.chart_path), + "split": _split_for_hash(audio_hash), + "audioFile": "audio.mp3", + "beatforgeFile": "beatforge.json", + "chartFile": "chart.json", + "realData": True, + } + _write_json(sample_dir / "metadata.json", metadata) + manifest_samples.append(metadata) + built_charts.append(chart) + report.samples.append(asset.id) + report.completed += 1 + except Exception as exc: # each real song should leave an auditable failure + report.failed += 1 + report.errors.append( + {"chartId": asset.id, "code": type(exc).__name__, "message": str(exc)} + ) + if built_charts: + statistics = corpus_statistics(built_charts) + _write_json( + output / "chart_statistics.json", + statistics.model_dump(by_alias=True, mode="json"), + ) + _write_json( + output / "manifest.json", + { + "schemaVersion": "beatforge.chart-training-dataset.v1", + "realDataOnly": True, + "mode": mode, + "analysisMode": analysis_mode, + "sampleCount": len(manifest_samples), + "uniqueAudioCount": len({sample["audioSha256"] for sample in manifest_samples}), + "splits": { + split: sum(sample["split"] == split for sample in manifest_samples) + for split in ("train", "validation", "test") + }, + "samples": manifest_samples, + }, + ) + _write_json(output / "build_report.json", report.to_dict()) + return report diff --git a/apps/api/beatforge_api/chart_engine/footwork.py b/apps/api/beatforge_api/chart_engine/footwork.py new file mode 100644 index 0000000..dee8ea7 --- /dev/null +++ b/apps/api/beatforge_api/chart_engine/footwork.py @@ -0,0 +1,603 @@ +from __future__ import annotations + +import math +from dataclasses import dataclass +from itertools import permutations +from typing import Literal + +from .models import ChartEvent + +Foot = Literal["left", "right"] +LaneEvidenceKey = tuple[float, float] +LaneProbabilities = tuple[float, float, float, float, float] + +_COORDS = ((-1.0, -1.0), (-1.0, 1.0), (0.0, 0.0), (1.0, 1.0), (1.0, -1.0)) +_RESET_GAP_SEC = 0.60 +_HEADING_LIMIT_DEGREES = 135.0 +_MAX_HEADING_STEP_DEGREES = 180.0 +_EPSILON = 1e-9 +_SPIN_PATTERNS = {"small_spin", "big_spin"} + + +@dataclass(frozen=True, slots=True) +class FootPose: + left_lane: int | None = None + right_lane: int | None = None + heading: float = 0.0 + last_strike: Foot | None = None + left_hold_until: float = 0.0 + right_hold_until: float = 0.0 + + +@dataclass(frozen=True, slots=True) +class FootworkViolation: + event_index: int + time_sec: float + beat: float + + +@dataclass(frozen=True, slots=True) +class FootworkAnalysis: + full_step_reachable: bool + checked_events: int + segment_count: int + violations: tuple[FootworkViolation, ...] + max_abs_heading: float + crossover_count: int + hold_forced_repeats: int + + +@dataclass(frozen=True, slots=True) +class FootworkRepairReport: + lanes_reassigned: int + feet_assigned: int + notes_removed: int + segments_repaired: int + + +@dataclass(frozen=True, slots=True) +class _Transition: + pose: FootPose + feet: tuple[Foot, ...] + turn: float + crossover: int + hold_forced_repeat: int + + +@dataclass(frozen=True, slots=True) +class _Choice: + playable_indices: tuple[int, ...] + lanes: tuple[int, ...] + feet: tuple[Foot, ...] + + +@dataclass(frozen=True, slots=True) +class _Path: + # Keep removals and lane edits lexicographically ahead of style costs. + score: tuple[int, int, int, float, float, float] + previous: _Path | None + choice: _Choice | None + depth: int + max_abs_heading: float + crossover_count: int + hold_forced_repeats: int + + +def lane_evidence_key(event: ChartEvent) -> LaneEvidenceKey: + return (round(event.time_sec, 9), round(event.beat, 9)) + + +def _playable_positions(event: ChartEvent) -> tuple[int, ...]: + return tuple(index for index, note in enumerate(event.notes) if note.type != "mine") + + +def _released(pose: FootPose, time_sec: float) -> FootPose: + left_hold = pose.left_hold_until if pose.left_hold_until > time_sec + _EPSILON else 0.0 + right_hold = pose.right_hold_until if pose.right_hold_until > time_sec + _EPSILON else 0.0 + if left_hold == pose.left_hold_until and right_hold == pose.right_hold_until: + return pose + return FootPose( + left_lane=pose.left_lane, + right_lane=pose.right_lane, + heading=pose.heading, + last_strike=pose.last_strike, + left_hold_until=left_hold, + right_hold_until=right_hold, + ) + + +def _reset_pose() -> FootPose: + return FootPose() + + +def _heading_for_stance( + left_lane: int | None, + right_lane: int | None, + previous_heading: float, +) -> tuple[float, float] | None: + if left_lane is None or right_lane is None or left_lane == right_lane: + return previous_heading, 0.0 + left = _COORDS[left_lane] + right = _COORDS[right_lane] + raw = math.degrees(math.atan2(right[1] - left[1], right[0] - left[0])) + candidates = [] + for winding in (-1, 0, 1): + heading = raw + winding * 360.0 + turn = abs(heading - previous_heading) + if ( + abs(heading) <= _HEADING_LIMIT_DEGREES + _EPSILON + and turn <= _MAX_HEADING_STEP_DEGREES + _EPSILON + ): + candidates.append((turn, abs(heading), heading)) + if not candidates: + return None + turn, _absolute, heading = min(candidates) + return heading, turn + + +def _candidate_feet(pose: FootPose) -> tuple[Foot, ...]: + left_locked = pose.left_hold_until > 0.0 + right_locked = pose.right_hold_until > 0.0 + if left_locked and right_locked: + return () + if left_locked: + return ("right",) + if right_locked: + return ("left",) + if pose.last_strike == "left": + return ("right",) + if pose.last_strike == "right": + return ("left",) + return ("left", "right") + + +def _single_transitions( + pose: FootPose, + event: ChartEvent, + playable_index: int, + lane: int, +) -> tuple[_Transition, ...]: + pose = _released(pose, event.time_sec) + note = event.notes[playable_index] + output: list[_Transition] = [] + for foot in _candidate_feet(pose): + left_lane = pose.left_lane + right_lane = pose.right_lane + left_hold = pose.left_hold_until + right_hold = pose.right_hold_until + forced_repeat = int( + (left_hold > 0.0 or right_hold > 0.0) and pose.last_strike == foot + ) + if foot == "left": + if left_hold > 0.0: + continue + if right_lane == lane: + if right_hold > 0.0: + continue + right_lane = None + left_lane = lane + if note.type == "hold" and note.end_time_sec is not None: + left_hold = note.end_time_sec + else: + if right_hold > 0.0: + continue + if left_lane == lane: + if left_hold > 0.0: + continue + left_lane = None + right_lane = lane + if note.type == "hold" and note.end_time_sec is not None: + right_hold = note.end_time_sec + heading_result = _heading_for_stance(left_lane, right_lane, pose.heading) + if heading_result is None: + continue + heading, turn = heading_result + output.append( + _Transition( + pose=FootPose( + left_lane=left_lane, + right_lane=right_lane, + heading=heading, + last_strike=foot, + left_hold_until=left_hold, + right_hold_until=right_hold, + ), + feet=(foot,), + turn=turn, + crossover=int(abs(heading) > 90.0 + _EPSILON), + hold_forced_repeat=forced_repeat, + ) + ) + return tuple(output) + + +def _jump_transitions( + pose: FootPose, + event: ChartEvent, + playable_indices: tuple[int, int], + lanes: tuple[int, int], +) -> tuple[_Transition, ...]: + pose = _released(pose, event.time_sec) + if pose.left_hold_until > 0.0 or pose.right_hold_until > 0.0: + # A continuing hold plus two new panels would require three feet. + return () + output: list[_Transition] = [] + for left_note_position, right_note_position in permutations((0, 1)): + left_lane = lanes[left_note_position] + right_lane = lanes[right_note_position] + heading_result = _heading_for_stance(left_lane, right_lane, pose.heading) + if heading_result is None: + continue + heading, turn = heading_result + feet: list[Foot] = ["left", "left"] + feet[left_note_position] = "left" + feet[right_note_position] = "right" + left_note = event.notes[playable_indices[left_note_position]] + right_note = event.notes[playable_indices[right_note_position]] + left_hold = ( + left_note.end_time_sec + if left_note.type == "hold" and left_note.end_time_sec is not None + else 0.0 + ) + right_hold = ( + right_note.end_time_sec + if right_note.type == "hold" and right_note.end_time_sec is not None + else 0.0 + ) + output.append( + _Transition( + pose=FootPose( + left_lane=left_lane, + right_lane=right_lane, + heading=heading, + last_strike=None, + left_hold_until=left_hold, + right_hold_until=right_hold, + ), + feet=(feet[0], feet[1]), + turn=turn, + crossover=int(abs(heading) > 90.0 + _EPSILON), + hold_forced_repeat=0, + ) + ) + return tuple(output) + + +def _fixed_transitions(pose: FootPose, event: ChartEvent) -> tuple[_Transition, ...]: + playable = _playable_positions(event) + if not playable: + return ( + _Transition( + pose=_released(pose, event.time_sec), + feet=(), + turn=0.0, + crossover=0, + hold_forced_repeat=0, + ), + ) + if len(playable) == 1: + return _single_transitions( + pose, event, playable[0], event.notes[playable[0]].lane + ) + if len(playable) == 2: + lanes = (event.notes[playable[0]].lane, event.notes[playable[1]].lane) + return _jump_transitions(pose, event, (playable[0], playable[1]), lanes) + return () + + +def analyze_no_spin_footwork(events: list[ChartEvent]) -> FootworkAnalysis: + ordered = sorted(enumerate(events), key=lambda item: (item[1].time_sec, item[1].beat)) + poses: dict[FootPose, tuple[float, int, int]] = {_reset_pose(): (0.0, 0, 0)} + violations: list[FootworkViolation] = [] + checked_events = 0 + segment_count = 1 if ordered else 0 + previous_time: float | None = None + + for original_index, event in ordered: + if event.pattern in _SPIN_PATTERNS: + poses = {_reset_pose(): (0.0, 0, 0)} + previous_time = event.time_sec + continue + if previous_time is not None and event.time_sec - previous_time >= _RESET_GAP_SEC: + if all( + pose.left_hold_until <= event.time_sec + _EPSILON + and pose.right_hold_until <= event.time_sec + _EPSILON + for pose in poses + ): + poses = {_reset_pose(): min(poses.values())} + segment_count += 1 + previous_time = event.time_sec + if _playable_positions(event): + checked_events += 1 + + next_poses: dict[FootPose, tuple[float, int, int]] = {} + for pose, metrics in poses.items(): + for transition in _fixed_transitions(pose, event): + candidate = ( + max(metrics[0], abs(transition.pose.heading)), + metrics[1] + transition.crossover, + metrics[2] + transition.hold_forced_repeat, + ) + previous = next_poses.get(transition.pose) + if previous is None or candidate < previous: + next_poses[transition.pose] = candidate + if next_poses: + poses = next_poses + continue + + violations.append( + FootworkViolation( + event_index=original_index, + time_sec=event.time_sec, + beat=event.beat, + ) + ) + # Resume auditing at the failed row so one bad phrase does not make the + # remainder of the song look like hundreds of independent failures. + prior_metrics = min(poses.values()) + reset_transitions = _fixed_transitions(_reset_pose(), event) + poses = { + transition.pose: ( + max(prior_metrics[0], abs(transition.pose.heading)), + prior_metrics[1] + transition.crossover, + prior_metrics[2] + transition.hold_forced_repeat, + ) + for transition in reset_transitions + } or {_reset_pose(): (0.0, 0, 0)} + segment_count += 1 + + best_metrics = min(poses.values(), default=(0.0, 0, 0)) + return FootworkAnalysis( + full_step_reachable=not violations, + checked_events=checked_events, + segment_count=segment_count, + violations=tuple(violations), + max_abs_heading=best_metrics[0], + crossover_count=best_metrics[1], + hold_forced_repeats=best_metrics[2], + ) + + +def _path_key(path: _Path) -> tuple[int, int, int, float, float, float]: + return path.score + + +def _add_path( + target: dict[FootPose, _Path], + pose: FootPose, + path: _Path, +) -> None: + previous = target.get(pose) + if previous is None or _path_key(path) < _path_key(previous): + target[pose] = path + + +def _extended_path( + path: _Path, + transition: _Transition, + choice: _Choice, + *, + removed_notes: int, + changed_events: int, + changed_notes: int, + model_loss: float, + distance: float, +) -> _Path: + score = ( + path.score[0] + removed_notes, + path.score[1] + changed_events, + path.score[2] + changed_notes, + path.score[3] + model_loss, + path.score[4] + distance, + path.score[5] + transition.turn, + ) + return _Path( + score=score, + previous=path, + choice=choice, + depth=path.depth + 1, + max_abs_heading=max(path.max_abs_heading, abs(transition.pose.heading)), + crossover_count=path.crossover_count + transition.crossover, + hold_forced_repeats=path.hold_forced_repeats + transition.hold_forced_repeat, + ) + + +def _lane_cost( + original_lane: int, + lane: int, + probabilities: LaneProbabilities | None, +) -> tuple[int, int, float, float]: + changed = int(original_lane != lane) + model_loss = 0.0 + if probabilities is not None: + model_loss = max(probabilities) - probabilities[lane] + original = _COORDS[original_lane] + candidate = _COORDS[lane] + distance = math.hypot(candidate[0] - original[0], candidate[1] - original[1]) + return changed, changed, model_loss, distance + + +def repair_no_spin_footwork( + events: list[ChartEvent], + *, + lane_probabilities: dict[LaneEvidenceKey, LaneProbabilities] | None = None, +) -> tuple[list[ChartEvent], FootworkRepairReport]: + """Viterbi-decode a strict alternating, no-spin path without deleting rows.""" + + ordered = sorted(events, key=lambda item: (item.time_sec, item.beat)) + paths: dict[FootPose, _Path] = { + _reset_pose(): _Path( + score=(0, 0, 0, 0.0, 0.0, 0.0), + previous=None, + choice=None, + depth=0, + max_abs_heading=0.0, + crossover_count=0, + hold_forced_repeats=0, + ) + } + previous_time: float | None = None + for event in ordered: + if event.pattern in _SPIN_PATTERNS: + best = min(paths.values(), key=_path_key) + paths = {_reset_pose(): best} + previous_time = event.time_sec + elif previous_time is not None and event.time_sec - previous_time >= _RESET_GAP_SEC: + resettable = [ + path + for pose, path in paths.items() + if pose.left_hold_until <= event.time_sec + _EPSILON + and pose.right_hold_until <= event.time_sec + _EPSILON + ] + if resettable: + paths = {_reset_pose(): min(resettable, key=_path_key)} + previous_time = event.time_sec + playable = _playable_positions(event) + probabilities = (lane_probabilities or {}).get(lane_evidence_key(event)) + next_paths: dict[FootPose, _Path] = {} + + if not playable: + for pose, path in paths.items(): + transition = _fixed_transitions(pose, event)[0] + _add_path( + next_paths, + transition.pose, + _extended_path( + path, + transition, + _Choice((), (), ()), + removed_notes=0, + changed_events=0, + changed_notes=0, + model_loss=0.0, + distance=0.0, + ), + ) + elif len(playable) == 1: + original_lane = event.notes[playable[0]].lane + for pose, path in paths.items(): + for lane in range(5): + lane_cost = _lane_cost(original_lane, lane, probabilities) + for transition in _single_transitions(pose, event, playable[0], lane): + choice = _Choice((playable[0],), (lane,), transition.feet) + _add_path( + next_paths, + transition.pose, + _extended_path( + path, + transition, + choice, + removed_notes=0, + changed_events=lane_cost[0], + changed_notes=lane_cost[1], + model_loss=lane_cost[2], + distance=lane_cost[3], + ), + ) + elif len(playable) == 2: + original_lanes = ( + event.notes[playable[0]].lane, + event.notes[playable[1]].lane, + ) + for pose, path in paths.items(): + for transition in _jump_transitions( + pose, event, (playable[0], playable[1]), original_lanes + ): + choice = _Choice(playable, original_lanes, transition.feet) + _add_path( + next_paths, + transition.pose, + _extended_path( + path, + transition, + choice, + removed_notes=0, + changed_events=0, + changed_notes=0, + model_loss=0.0, + distance=0.0, + ), + ) + # If a continuing hold makes a jump impossible, preserve its + # strongest rhythm note instead of deleting the timing row. + if not _jump_transitions( + pose, event, (playable[0], playable[1]), original_lanes + ): + retained = max( + playable, + key=lambda index: event.notes[index].confidence, + ) + original_lane = event.notes[retained].lane + for lane in range(5): + lane_cost = _lane_cost(original_lane, lane, probabilities) + for transition in _single_transitions(pose, event, retained, lane): + choice = _Choice((retained,), (lane,), transition.feet) + _add_path( + next_paths, + transition.pose, + _extended_path( + path, + transition, + choice, + removed_notes=1, + changed_events=lane_cost[0], + changed_notes=lane_cost[1], + model_loss=lane_cost[2], + distance=lane_cost[3], + ), + ) + if not next_paths: + raise ValueError( + f"no full-step lane repair is possible at beat {event.beat:.6f}" + ) + paths = next_paths + + best = min(paths.values(), key=_path_key) + choices: list[_Choice] = [] + cursor: _Path | None = best + while cursor is not None and cursor.choice is not None: + choices.append(cursor.choice) + cursor = cursor.previous + choices.reverse() + if best.depth != len(ordered) or len(choices) != len(ordered): + raise ValueError("footwork decoder lost event alignment") + output: list[ChartEvent] = [] + lanes_reassigned = 0 + feet_assigned = 0 + notes_removed = 0 + segments_repaired = 0 + segment_changed = False + previous_time = None + for event, choice in zip(ordered, choices, strict=True): + if previous_time is not None and event.time_sec - previous_time >= _RESET_GAP_SEC: + segments_repaired += int(segment_changed) + segment_changed = False + previous_time = event.time_sec + selected = { + index: (lane, foot) + for index, lane, foot in zip( + choice.playable_indices, choice.lanes, choice.feet, strict=True + ) + } + updated_notes = [] + for index, note in enumerate(event.notes): + if note.type == "mine": + updated_notes.append(note) + continue + if index not in selected: + notes_removed += 1 + continue + lane, foot = selected[index] + lanes_reassigned += int(lane != note.lane) + feet_assigned += int(note.foot != foot) + segment_changed = segment_changed or lane != note.lane + updated_notes.append(note.model_copy(update={"lane": lane, "foot": foot})) + segment_changed = segment_changed or len(selected) < len(_playable_positions(event)) + output.append(event.model_copy(update={"notes": updated_notes})) + segments_repaired += int(segment_changed) + return output, FootworkRepairReport( + lanes_reassigned=lanes_reassigned, + feet_assigned=feet_assigned, + notes_removed=notes_removed, + segments_repaired=segments_repaired, + ) diff --git a/apps/api/beatforge_api/chart_engine/generator.py b/apps/api/beatforge_api/chart_engine/generator.py new file mode 100644 index 0000000..58b7306 --- /dev/null +++ b/apps/api/beatforge_api/chart_engine/generator.py @@ -0,0 +1,829 @@ +from __future__ import annotations + +import hashlib +import math +import random +from collections import defaultdict +from collections.abc import Iterable +from dataclasses import asdict, dataclass +from typing import Any + +from .footwork import LaneEvidenceKey, LaneProbabilities, lane_evidence_key +from .models import ChartDocument, ChartEvent, ChartNote +from .optimizer import optimize_events +from .statistics import chart_statistics +from .timing import TempoTimeline +from .validator import validate_chart + +_DEFAULT_TRANSITIONS = ( + (0.03, 0.12, 0.30, 0.28, 0.27), + (0.12, 0.03, 0.30, 0.27, 0.28), + (0.24, 0.24, 0.04, 0.24, 0.24), + (0.28, 0.27, 0.30, 0.03, 0.12), + (0.27, 0.28, 0.30, 0.12, 0.03), +) +_COORDS = ((-1.0, -1.0), (-1.0, 1.0), (0.0, 0.0), (1.0, 1.0), (1.0, -1.0)) +_BIG_SPIN = (3, 2, 4, 2, 1, 2, 0, 2) +_SMALL_SPIN = (3, 2, 4) * 3 +_MODEL_EVENT_THRESHOLD = 0.5 +_GENERATOR_VERSION = "1.7" + + +@dataclass(slots=True) +class _SourcePoint: + beat: float + time_sec: float + score: float + source_id: str | None + source: str + subdivision: int + lane_probabilities: tuple[float, float, float, float, float] | None = None + hold_probability: float | None = None + source_event_ids: tuple[str, ...] = () + source_hit_point_ids: tuple[str, ...] = () + anchor_priority: int = 0 + is_full_band_accent: bool = False + + +def _value(item: Any, *names: str, default: Any = None) -> Any: + for name in names: + if isinstance(item, dict) and name in item: + return item[name] + if hasattr(item, name): + return getattr(item, name) + return default + + +def _is_vocal_mora_anchor(item: Any) -> bool: + """Return whether a published HuBERT mora is chart rhythm evidence. + + The vocal editor renders both accepted and uncertain HuBERT mora events as + aligned timing markers. ``uncertain`` describes confidence, not the + absence of a vocal onset, so the chart model may choose its panel but must + not delete its timing row. Explicitly rejected events remain optional. + """ + + status = str(_value(item, "status", default="uncertain")).lower() + generator = str(_value(item, "generator", default="")).lower() + event_level = str(_value(item, "event_level", "eventLevel", default="")).lower() + source = str(_value(item, "source", "lane", default="")).lower() + alignment_unit_id = _value( + item, "alignment_unit_id", "alignmentUnitId", default=None + ) + alignment_unit_index = _value( + item, "alignment_unit_index", "alignmentUnitIndex", default=None + ) + alignment_run_id = _value( + item, "alignment_run_id", "alignmentRunId", default=None + ) + return ( + status != "rejected" + and generator == "hubert_ctc" + and event_level == "mora" + and source == "vocals" + and bool(alignment_unit_id) + and alignment_unit_index is not None + and bool(alignment_run_id) + ) + + +def _tempo_timeline( + tempo_segments: Iterable[Any], sample_rate: int +) -> tuple[TempoTimeline, list[tuple[float, float]]]: + segments = sorted( + list(tempo_segments), + key=lambda item: int(_value(item, "start_sample", "startSample", default=0)), + ) + if not segments: + raise ValueError("chart generation requires a BeatForge tempo map") + first = segments[0] + first_bpm = float(_value(first, "bpm", default=0.0)) + if first_bpm <= 0: + raise ValueError("chart generation requires a positive BPM") + beat_zero_sec = ( + float(_value(first, "beat_offset_sample", "beatOffsetSample", default=0)) / sample_rate + ) + changes: list[tuple[float, float]] = [(0.0, first_bpm)] + previous_time = 0.0 + previous_beat = -beat_zero_sec * first_bpm / 60.0 + previous_bpm = first_bpm + for segment in segments[1:]: + start_time = float(_value(segment, "start_sample", "startSample", default=0)) / sample_rate + beat = previous_beat + (start_time - previous_time) * previous_bpm / 60.0 + bpm = float(_value(segment, "bpm", default=previous_bpm)) + if beat >= 0 and bpm > 0: + changes.append((beat, bpm)) + previous_time = start_time + previous_beat = beat + previous_bpm = bpm + return TempoTimeline(changes, offset_sec=-beat_zero_sec), changes + + +def _grid_subdivision(difficulty: int) -> int: + if difficulty <= 3: + return 4 + if difficulty <= 7: + return 8 + # Lv.8-10 use at most sixteenths. Lv.11+ may additionally select + # evidence-backed twenty-fourths, but inferred filler stays on sixteenths. + return 16 + + +def _grid_step(difficulty: int) -> float: + return 4.0 / _grid_subdivision(difficulty) + + +def _snap_beat(beat: float, subdivision: int) -> float: + step = 4.0 / subdivision + return round(beat / step) * step + + +def _slot_key(beat: float) -> int: + """Index the union of 1/16 and 1/24 grids without float-key drift.""" + + # Sixteenth rows are multiples of 3/12 beat and twenty-fourth rows are + # multiples of 2/12 beat, so 1/12 beat is their exact common lattice. + return int(round(beat * 12.0)) + + +def _source_grid( + item: Any, + *, + kind: str, + difficulty: int, + timeline: TempoTimeline, + sample_rate: int, + chart_sample: int, +) -> tuple[float, int]: + chart_beat = timeline.time_to_beat(chart_sample / sample_rate) + base_subdivision = _grid_subdivision(difficulty) + if difficulty <= 10: + return _snap_beat(chart_beat, base_subdivision), base_subdivision + + grid_type = str(_value(item, "grid_type", "gridType", default="")).lower() + grid_confidence = float( + _value(item, "grid_confidence", "gridConfidence", default=0.0) or 0.0 + ) + if grid_confidence >= 0.5 and ("1_24" in grid_type or "twenty_fourth" in grid_type): + return _snap_beat(chart_beat, 24), 24 + if grid_confidence >= 0.5 and ("1_16" in grid_type or "sixteenth" in grid_type): + return _snap_beat(chart_beat, 16), 16 + + # BeatForge currently emits predominantly 1/16 chart samples. At Lv.11+ + # only low-confidence/unsnapped candidate evidence may recover a 1/24 row + # from its acoustic position. Confirmed hit points retain their edited chart + # timing and are snapped to the nearest supported row. + source_sample = chart_sample + if kind == "candidate": + source_sample = int( + _value( + item, + "acoustic_sample", + "acousticSample", + "refined_sample", + "refinedSample", + "sample", + default=chart_sample, + ) + ) + source_beat = timeline.time_to_beat(source_sample / sample_rate) + choices = [ + (abs(source_beat - _snap_beat(source_beat, subdivision)), subdivision) + for subdivision in (16, 24) + ] + _error, subdivision = min(choices, key=lambda value: (value[0], value[1])) + return _snap_beat(source_beat, subdivision), subdivision + + +def _merge_ids(*groups: tuple[str, ...]) -> tuple[str, ...]: + return tuple(dict.fromkeys(item for group in groups for item in group if item)) + + +def _merge_source_points(previous: _SourcePoint, point: _SourcePoint) -> _SourcePoint: + winner, other = max( + ((previous, point), (point, previous)), + key=lambda pair: (pair[0].anchor_priority, pair[0].score), + ) + source_event_ids = _merge_ids(previous.source_event_ids, point.source_event_ids) + source_hit_point_ids = _merge_ids( + previous.source_hit_point_ids, point.source_hit_point_ids + ) + source_id = winner.source_id + if source_event_ids and source_id not in source_event_ids: + source_id = source_event_ids[0] + return _SourcePoint( + beat=winner.beat, + time_sec=winner.time_sec, + score=winner.score, + source_id=source_id, + source="hit_point" if source_hit_point_ids else winner.source, + subdivision=min(previous.subdivision, point.subdivision), + lane_probabilities=winner.lane_probabilities or other.lane_probabilities, + hold_probability=( + winner.hold_probability + if winner.hold_probability is not None + else other.hold_probability + ), + source_event_ids=source_event_ids, + source_hit_point_ids=source_hit_point_ids, + anchor_priority=max(previous.anchor_priority, point.anchor_priority), + is_full_band_accent=( + previous.is_full_band_accent or point.is_full_band_accent + ), + ) + + +def _source_points( + *, + timeline: TempoTimeline, + sample_rate: int, + duration_sec: float, + candidates: Iterable[Any], + hit_points: Iterable[Any], + difficulty: int, + model_predictions: dict[str, dict[str, Any]] | None, +) -> list[_SourcePoint]: + by_slot: dict[int, _SourcePoint] = {} + candidate_items = list(candidates) + hit_items = list(hit_points) + candidate_by_hit_id = { + str(hit_point_id): str(candidate_id) + for candidate in candidate_items + if (candidate_id := _value(candidate, "id")) + and (hit_point_id := _value(candidate, "hit_point_id", "hitPointId", default=None)) + } + + def add(item: Any, kind: str, bonus: float) -> None: + sample = int( + _value( + item, + "chart_sample", + "chartSample", + "snapped_sample", + "snappedSample", + "sample", + default=-1, + ) + ) + time_sec = sample / sample_rate + if sample < 0 or time_sec < 0 or time_sec > duration_sec: + return + beat = timeline.time_to_beat(time_sec) + if beat < 0 and kind != "hit_point": + return + if beat < 0: + # A confirmed marker can legitimately sit in the short pickup + # before BeatForge's first beat. Chart rows cannot have a negative + # beat, so preserve it at the beat-zero boundary instead of + # silently deleting it. + snapped_beat = 0.0 + subdivision = _grid_subdivision(difficulty) + else: + snapped_beat, subdivision = _source_grid( + item, + kind=kind, + difficulty=difficulty, + timeline=timeline, + sample_rate=sample_rate, + chart_sample=sample, + ) + snapped_time = timeline.beat_to_time(snapped_beat) + if snapped_time < 0 or snapped_time > duration_sec: + return + confidence = float(_value(item, "confidence", default=0.5) or 0.0) + grid_confidence = float( + _value(item, "grid_confidence", "gridConfidence", default=0.5) or 0.0 + ) + salience = float(_value(item, "salience", default=confidence) or 0.0) + status = str(_value(item, "status", default="accepted")) + primary = str(_value(item, "primary_stem", "primaryStem", "lane", "source", default="mix")) + source_weight = {"drums": 0.12, "vocals": 0.08, "melody": 0.06, "other": 0.06}.get( + primary, 0.03 + ) + score = ( + 0.48 * confidence + + 0.24 * grid_confidence + + 0.18 * salience + + source_weight + + bonus + + (0.06 if status == "accepted" else -0.08 if status == "rejected" else 0.0) + ) + slot = _slot_key(snapped_beat) + item_id = str(_value(item, "id", default="")) or None + prediction_id = item_id + if kind == "hit_point": + prediction_id = ( + str( + _value( + item, + "candidate_event_id", + "candidateEventId", + default=candidate_by_hit_id.get(item_id or ""), + ) + or "" + ) + or None + ) + prediction = (model_predictions or {}).get(prediction_id or "", {}) + raw_lanes = prediction.get("laneProbabilities") + lane_probabilities = None + anchor_priority = ( + 2 + if kind == "hit_point" + else 1 + if status == "accepted" or _is_vocal_mora_anchor(item) + else 0 + ) + if isinstance(raw_lanes, list | tuple) and len(raw_lanes) == 5: + lane_probabilities = tuple(min(1.0, max(0.0, float(value))) for value in raw_lanes) + # The current model has five lane sigmoid heads, not an event- + # presence head. Low absolute lane confidence can filter optional + # proposals, but must never erase accepted/confirmed rhythm input. + if ( + anchor_priority == 0 + and max(lane_probabilities) < _MODEL_EVENT_THRESHOLD + ): + return + score += 0.35 * (max(lane_probabilities) - 0.5) + raw_hold = prediction.get("holdProbability") + hold_probability = min(1.0, max(0.0, float(raw_hold))) if raw_hold is not None else None + point = _SourcePoint( + beat=snapped_beat, + time_sec=snapped_time, + score=score, + source_id=prediction_id if lane_probabilities is not None else item_id, + source=kind, + subdivision=subdivision, + lane_probabilities=lane_probabilities, + hold_probability=hold_probability, + source_event_ids=( + (item_id,) + if kind == "candidate" and item_id + else (prediction_id,) + if prediction_id + else () + ), + source_hit_point_ids=(item_id,) if kind == "hit_point" and item_id else (), + anchor_priority=anchor_priority, + is_full_band_accent=( + kind == "hit_point" + and str(_value(item, "band", default="")) == "full_band_accent" + ), + ) + previous = by_slot.get(slot) + if previous is None: + by_slot[slot] = point + else: + by_slot[slot] = _merge_source_points(previous, point) + + for candidate in candidate_items: + add(candidate, "candidate", 0.0) + for hit in hit_items: + add(hit, "hit_point", 0.10) + return sorted(by_slot.values(), key=lambda point: (point.beat, -point.score)) + + +def _choose_points( + points: list[_SourcePoint], + *, + difficulty: int, + duration_sec: float, + timeline: TempoTimeline, + rng: random.Random, + fill_from_tempo_grid: bool, +) -> list[_SourcePoint]: + if not points: + raise ValueError("chart generation requires analyzed candidate events or hit points") + target_nps = 0.45 + difficulty * 0.21 + active_duration = min(duration_sec, max(1.0, points[-1].time_sec - points[0].time_sec + 2.0)) + target = max(8, int(round(active_duration * target_nps))) + protected = [point for point in points if point.anchor_priority > 0] + optional = [point for point in points if point.anchor_priority == 0] + optional_budget = max(0, target - len(protected)) + if len(optional) > optional_budget: + ratio = optional_budget / len(optional) if optional else 0.0 + buckets: defaultdict[int, list[_SourcePoint]] = defaultdict(list) + for point in optional: + buckets[int(point.beat // 16.0)].append(point) + chosen: list[_SourcePoint] = [] + for bucket in buckets.values(): + if optional_budget == 0: + break + quota = max(1, min(len(bucket), int(round(len(bucket) * ratio)))) + ranked = sorted( + bucket, + key=lambda point: point.score + rng.random() * 0.035, + reverse=True, + ) + chosen.extend(ranked[:quota]) + if len(chosen) > optional_budget: + chosen = sorted( + chosen, key=lambda point: point.score + rng.random() * 0.02, reverse=True + )[:optional_budget] + elif len(chosen) < optional_budget: + chosen_ids = {id(point) for point in chosen} + remaining = [point for point in optional if id(point) not in chosen_ids] + chosen.extend( + sorted( + remaining, + key=lambda point: point.score + rng.random() * 0.02, + reverse=True, + )[: optional_budget - len(chosen)] + ) + points = sorted(protected + chosen, key=lambda point: point.beat) + if len(points) < target and fill_from_tempo_grid: + # Fill only gaps inside analyzed coverage and only on the real tempo grid. + step = _grid_step(difficulty) + subdivision = _grid_subdivision(difficulty) + occupied = {_slot_key(point.beat) for point in points} + first_slot = int(math.ceil(points[0].beat / step - 1e-9)) + last_slot = int(math.floor(points[-1].beat / step + 1e-9)) + additions: list[_SourcePoint] = [] + for slot in range(first_slot, last_slot + 1): + beat = slot * step + if len(points) + len(additions) >= target or _slot_key(beat) in occupied: + continue + # Prefer downbeats and half-beats; finer inferred grid points receive + # a lower score and are used only at high requested difficulties. + phase = beat % 1.0 + if phase and difficulty < 8: + continue + time_sec = timeline.beat_to_time(beat) + if 0 <= time_sec <= duration_sec: + additions.append( + _SourcePoint( + beat=beat, + time_sec=time_sec, + score=0.34 if phase == 0 else 0.24, + source_id=None, + source="tempo_grid", + subdivision=subdivision, + ) + ) + points = sorted(points + additions, key=lambda point: point.beat) + return points + + +def _weighted_lane( + rng: random.Random, + previous_lane: int | None, + previous_time: float | None, + time_sec: float, + transitions: list[list[float]], + previous_panel_side: str | None, + model_probabilities: tuple[float, float, float, float, float] | None, +) -> int: + weights = list(transitions[previous_lane] if previous_lane is not None else [1.0] * 5) + if model_probabilities is not None: + transition_total = max(sum(weights), 1e-9) + weights = [ + 0.25 * (weights[index] / transition_total) + 0.75 * model_probabilities[index] + for index in range(5) + ] + interval = time_sec - previous_time if previous_time is not None else 1.0 + for lane in range(5): + if lane == previous_lane: + weights[lane] *= 0.04 + if previous_lane is not None and interval < 0.12: + distance = math.dist(_COORDS[previous_lane], _COORDS[lane]) + if distance > 2.1: + weights[lane] *= 0.02 + preferred = "left" if lane in {0, 1} else "right" if lane in {3, 4} else None + if ( + interval < 0.24 + and preferred is not None + and preferred == previous_panel_side + ): + weights[lane] *= 0.18 + total = sum(max(0.0, weight) for weight in weights) + pick = rng.random() * total + cursor = 0.0 + for lane, weight in enumerate(weights): + cursor += max(0.0, weight) + if pick <= cursor: + return lane + return 2 + + +def _assign_events( + points: list[_SourcePoint], + *, + difficulty: int, + timeline: TempoTimeline, + transitions: list[list[float]], + rng: random.Random, +) -> tuple[ + list[ChartEvent], + dict[LaneEvidenceKey, LaneProbabilities], +]: + # Lv.6-10 model jumps must come from two independent lane heads. Randomly + # decorating otherwise isolated rows creates tiring, unmusical jump spam; + # a full-band BeatForge accent on an integer beat is the acoustic exception. + # Expert charts may retain a very small corpus-style fallback probability. + jump_probability = max(0.0, (difficulty - 10) * 0.008) + hold_probability = 0.015 + difficulty * 0.0035 + events: list[ChartEvent] = [] + lane_evidence: dict[LaneEvidenceKey, LaneProbabilities] = {} + previous_lane: int | None = None + previous_time: float | None = None + previous_panel_side: str | None = None + for index, point in enumerate(points): + step = 4.0 / point.subdivision + lane = _weighted_lane( + rng, + previous_lane, + previous_time, + point.time_sec, + transitions, + previous_panel_side, + point.lane_probabilities, + ) + interval = point.time_sec - previous_time if previous_time is not None else 1.0 + previous_beat_gap = ( + point.beat - points[index - 1].beat if index > 0 else math.inf + ) + next_beat_gap = ( + points[index + 1].beat - point.beat if index + 1 < len(points) else math.inf + ) + jump_spacing_safe = min(previous_beat_gap, next_beat_gap) >= 0.5 - 1e-9 + predicted_jump_pair: tuple[int, int] | None = None + if point.lane_probabilities is not None: + ranked_lanes = sorted( + range(5), key=lambda value: point.lane_probabilities[value], reverse=True + ) + # Five sigmoid heads describe independent panels. A jump exists only + # when the *second-highest* head is strong too; looking for a high + # lane relative to a randomly sampled primary falsely turns a single + # strong head into a jump. + if point.lane_probabilities[ranked_lanes[1]] >= 0.58: + predicted_jump_pair = (ranked_lanes[0], ranked_lanes[1]) + if jump_spacing_safe and lane not in predicted_jump_pair: + lane = predicted_jump_pair[0] + note_type = "tap" + end_beat = end_time = None + if index + 1 < len(points): + next_beat = points[index + 1].beat + effective_hold_probability = hold_probability + if point.hold_probability is not None: + effective_hold_probability = max( + hold_probability * 0.5, min(0.25, point.hold_probability * 0.35) + ) + if next_beat - point.beat >= 0.75 and rng.random() < effective_hold_probability: + end_beat = min(point.beat + 1.0, next_beat - step) + if end_beat - point.beat >= 0.5: + note_type = "hold" + end_time = timeline.beat_to_time(end_beat) + notes = [ + ChartNote( + lane=lane, + type=note_type, + end_time_sec=end_time, + end_beat=end_beat, + source=point.source, + confidence=min(1.0, max(0.0, point.score)), + foot=None, + ) + ] + predicted_jump_lane = None + if predicted_jump_pair is not None and lane in predicted_jump_pair: + predicted_jump_lane = next( + value for value in predicted_jump_pair if value != lane + ) + accent_jump = ( + difficulty >= 8 + and point.is_full_band_accent + and math.isclose(point.beat, round(point.beat), abs_tol=1e-9) + ) + if ( + difficulty >= 6 + and interval >= 0.13 + and jump_spacing_safe + and note_type == "tap" + and ( + predicted_jump_lane is not None + or accent_jump + or (jump_probability > 0.0 and rng.random() < jump_probability) + ) + ): + if predicted_jump_lane is not None: + second = predicted_jump_lane + else: + choices = [value for value in range(5) if value != lane and abs(value - lane) >= 2] + second = choices[int(rng.random() * len(choices))] + notes.append( + ChartNote( + lane=second, + source=point.source, + confidence=min(1.0, max(0.0, point.score)), + foot=None, + ) + ) + measure = int(point.beat // 4.0) + row_index = int( + round((point.beat - measure * 4.0) * point.subdivision / 4.0) + ) + event = ChartEvent( + time_sec=point.time_sec, + beat=point.beat, + measure=measure, + subdivision=point.subdivision, + row_index=row_index, + notes=notes, + source_event_id=point.source_id, + source_event_ids=list(point.source_event_ids), + source_hit_point_ids=list(point.source_hit_point_ids), + anchor_priority=point.anchor_priority, + ) + events.append(event) + if point.lane_probabilities is not None: + lane_evidence[lane_evidence_key(event)] = point.lane_probabilities + previous_lane = lane + previous_time = point.time_sec + previous_panel_side = ( + "left" if lane in {0, 1} else "right" if lane in {3, 4} else None + ) + return events, lane_evidence + + +def _apply_spin(events: list[ChartEvent], difficulty: int, enabled: bool) -> list[ChartEvent]: + if not enabled or difficulty < 11: + return events + output = list(events) + + def apply_pattern(start: int, pattern: tuple[int, ...], label: str) -> bool: + window = output[start : start + len(pattern)] + if len(window) != len(pattern): + return False + if any( + event.pattern or len(event.notes) != 1 or event.notes[0].type != "tap" + for event in window + ): + return False + if any( + current.beat - previous.beat > 0.5 + for previous, current in zip(window, window[1:], strict=False) + ): + return False + for index, (event, lane) in enumerate(zip(window, pattern, strict=True)): + foot = "left" if index % 2 == 0 else "right" + output[start + index] = event.model_copy( + update={ + "notes": [event.notes[0].model_copy(update={"lane": lane, "foot": foot})], + "pattern": label, + } + ) + return True + + def apply_near(target: int, pattern: tuple[int, ...], label: str) -> bool: + starts = range(0, max(0, len(output) - len(pattern)) + 1) + for start in sorted(starts, key=lambda value: abs(value - target)): + if apply_pattern(start, pattern, label): + return True + return False + + small_start = max(0, len(output) // 3 - len(_SMALL_SPIN) // 2) + apply_near(small_start, _SMALL_SPIN, "small_spin") + if difficulty >= 14: + big_start = max(0, (len(output) * 2) // 3 - len(_BIG_SPIN) // 2) + apply_near(big_start, _BIG_SPIN, "big_spin") + return output + + +def generate_chart( + *, + track_id: str, + title: str, + artist: str, + music: str, + duration_sec: float, + sample_rate: int, + tempo_segments: Iterable[Any], + candidates: Iterable[Any], + hit_points: Iterable[Any], + difficulty: int, + enable_spin: bool = False, + seed: int | None = None, + transition_probabilities: list[list[float]] | None = None, + model_predictions: dict[str, dict[str, Any]] | None = None, + model_provenance: dict[str, Any] | None = None, +) -> ChartDocument: + """Generate a deterministic five-panel chart from persisted BeatForge evidence.""" + + difficulty = min(max(int(difficulty), 1), 15) + if seed is None: + key = ( + f"{track_id}:{difficulty}:{int(enable_spin)}:" + f"beatforge-chart-v{_GENERATOR_VERSION}" + ) + seed = int.from_bytes(hashlib.sha256(key.encode()).digest()[:8], "big") + rng = random.Random(seed) + timeline, _changes = _tempo_timeline(tempo_segments, sample_rate) + candidate_items = list(candidates) + hit_point_items = list(hit_points) + vocal_mora_marker_ids = { + str(candidate_id) + for candidate in candidate_items + if _is_vocal_mora_anchor(candidate) + and (candidate_id := _value(candidate, "id", default=None)) + } + points = _source_points( + timeline=timeline, + sample_rate=sample_rate, + duration_sec=duration_sec, + candidates=candidate_items, + hit_points=hit_point_items, + difficulty=difficulty, + model_predictions=model_predictions, + ) + chosen = _choose_points( + points, + difficulty=difficulty, + duration_sec=duration_sec, + timeline=timeline, + rng=rng, + fill_from_tempo_grid=model_predictions is None, + ) + matrix = transition_probabilities or [list(row) for row in _DEFAULT_TRANSITIONS] + if len(matrix) != 5 or any(len(row) != 5 for row in matrix): + matrix = [list(row) for row in _DEFAULT_TRANSITIONS] + events, lane_evidence = _assign_events( + chosen, + difficulty=difficulty, + timeline=timeline, + transitions=matrix, + rng=rng, + ) + anchor_input = [event for event in events if event.anchor_priority > 0] + accepted_input = [event for event in events if event.anchor_priority == 1] + hit_point_input = [event for event in events if event.anchor_priority == 2] + events, optimization = optimize_events( + events, + difficulty, + bpm=timeline.primary_bpm, + lane_probabilities=lane_evidence, + ) + events = _apply_spin(events, difficulty, enable_spin) + output_source_event_ids = { + source_id for event in events for source_id in event.source_event_ids + } + optimization_payload = { + **asdict(optimization), + "protected_source_points_input": len(anchor_input), + "protected_source_points_output": sum( + event.anchor_priority > 0 for event in events + ), + "accepted_anchor_events_input": len(accepted_input), + "accepted_anchor_events_output": sum( + event.anchor_priority == 1 for event in events + ), + "hit_point_anchor_events_input": len(hit_point_input), + "hit_point_anchor_events_output": sum( + event.anchor_priority == 2 for event in events + ), + "vocal_mora_markers_input": len(vocal_mora_marker_ids), + "vocal_mora_markers_output": len( + vocal_mora_marker_ids & output_source_event_ids + ), + } + generator = "local_chart_transformer" if model_predictions else "real_corpus_profile_rules" + document_identity = ( + f"{track_id}:{difficulty}:{enable_spin}:{seed}:generator-{_GENERATOR_VERSION}" + ) + if model_predictions: + provenance = model_provenance or {} + checkpoint_identity = ":".join( + str(provenance.get(key) or "") + for key in ( + "checkpointSha256", + "architecture", + "createdAt", + "datasetFingerprint", + ) + ) + document_identity = f"{document_identity}:{generator}:{checkpoint_identity}" + document_id = hashlib.sha256(document_identity.encode()).hexdigest()[:20] + chart = ChartDocument( + id=document_id, + title=title, + artist=artist, + music=music, + source_group="BEATFORGE_GENERATED", + mode="pump-single", + lane_count=5, + difficulty="Challenge" if difficulty >= 13 else "Hard", + meter=difficulty, + bpm=timeline.primary_bpm, + offset_sec=timeline.offset_sec, + duration_sec=duration_sec, + measure_count=max((event.measure for event in events), default=0) + 1, + tempo_map=timeline.points(), + events=events, + optimization=optimization_payload, + model_provenance=model_provenance, + generator=generator, + generator_version=_GENERATOR_VERSION, + seed=seed, + spin_enabled=enable_spin, + ) + chart = chart.model_copy(update={"statistics": chart_statistics(chart)}) + return chart.model_copy(update={"validation": validate_chart(chart)}) diff --git a/apps/api/beatforge_api/chart_engine/learning.py b/apps/api/beatforge_api/chart_engine/learning.py new file mode 100644 index 0000000..0eb0379 --- /dev/null +++ b/apps/api/beatforge_api/chart_engine/learning.py @@ -0,0 +1,1075 @@ +from __future__ import annotations + +import bisect +import copy +import hashlib +import json +import math +import os +import random +import tempfile +from dataclasses import asdict, dataclass +from datetime import UTC, datetime +from pathlib import Path +from typing import Any, Literal + +from .model import ( + MODEL_ARCHITECTURE, + ChartTransformer, + ChartTransformerConfig, + require_torch, +) +from .models import ChartDocument + +FEATURE_SCHEMA_VERSION = "beatforge.chart-candidate-features.v1" +CHECKPOINT_SCHEMA_VERSION = "beatforge.chart-transformer.checkpoint.v1" +TRAINING_SAMPLE_SCHEMA_VERSION = "beatforge.chart-training-sample.v1" +TRAINING_FEATURE_SCHEMA_VERSION = "beatforge.chart-training-features.v1" + +FEATURE_NAMES = ( + "timePosition", + "previousGap2Sec", + "nextGap2Sec", + "candidateConfidence", + "gridConfidence", + "snapError100Ms", + "statusAccepted", + "statusUncertain", + "statusRejected", + "laneVocals", + "laneMelody", + "laneDrums", + "laneMix", + "sourceVocals", + "sourceMelody", + "sourceDrums", + "sourceMix", + "semanticLyricAlignment", + "semanticPhonemeConfidence", + "semanticPitchConfidence", + "semanticBeatConfidence", + "generatorAnalysis", + "generatorHubertCtc", + "localDensityHalfSecond", + "tempoBpm300", + "tempoConfidence", +) + +SplitName = Literal["train", "validation", "test"] + + +class DatasetContractError(ValueError): + """Raised when a training directory is not a complete real dataset sample.""" + + +class CheckpointContractError(ValueError): + """Raised when a local checkpoint does not match the supported model contract.""" + + +def _json_object(path: Path) -> dict[str, Any]: + try: + value = json.loads(path.read_text(encoding="utf-8")) + except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc: + raise DatasetContractError(f"cannot read dataset JSON {path}: {exc}") from exc + if not isinstance(value, dict): + raise DatasetContractError(f"dataset JSON must contain an object: {path}") + return value + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def _value(item: dict[str, Any], *names: str, default: Any = None) -> Any: + for name in names: + if name in item: + return item[name] + return default + + +def _number(value: Any, default: float = 0.0) -> float: + try: + parsed = float(value) + except (TypeError, ValueError): + return default + return parsed if math.isfinite(parsed) else default + + +def _unit(value: Any) -> float: + return min(1.0, max(0.0, _number(value))) + + +def _mapping(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + +def _safe_sample_asset(sample_dir: Path, relative_name: Any, label: str) -> Path: + if not isinstance(relative_name, str) or not relative_name.strip(): + raise DatasetContractError(f"{sample_dir.name} has no {label} file name") + candidate = (sample_dir / relative_name).resolve() + if not candidate.is_relative_to(sample_dir.resolve()): + raise DatasetContractError(f"{sample_dir.name} {label} path escapes the sample directory") + if not candidate.is_file() or candidate.stat().st_size <= 0: + raise DatasetContractError(f"{sample_dir.name} {label} file is missing or empty") + return candidate + + +@dataclass(frozen=True, slots=True) +class RealDatasetSample: + sample_id: str + split: SplitName + source_group: str + source_difficulty: int + training_difficulty: int + difficulty_was_clamped: bool + audio_sha256: str + metadata: dict[str, Any] + beatforge: dict[str, Any] + chart: ChartDocument + sample_dir: Path + + +def load_completed_dataset_samples( + dataset_dir: str | Path, + *, + split: SplitName | None = None, + verify_audio_hashes: bool = True, +) -> list[RealDatasetSample]: + """Load complete real samples emitted by :mod:`chart_engine.dataset` only. + + No arrays, targets, or placeholder candidates are synthesized when files are + absent. Any directory declaring itself as a sample must satisfy the complete + audio/analysis/chart contract before it can enter training. + """ + + root = Path(dataset_dir).expanduser().resolve() + if not root.is_dir(): + raise DatasetContractError(f"chart dataset directory does not exist: {root}") + samples: list[RealDatasetSample] = [] + for sample_dir in sorted(path for path in root.iterdir() if path.is_dir()): + metadata_path = sample_dir / "metadata.json" + if not metadata_path.is_file(): + continue + metadata = _json_object(metadata_path) + if metadata.get("schemaVersion") != TRAINING_SAMPLE_SCHEMA_VERSION: + raise DatasetContractError( + f"{sample_dir.name} uses an unsupported training sample schema" + ) + if metadata.get("realData") is not True: + raise DatasetContractError(f"{sample_dir.name} is not marked as real data") + sample_split = metadata.get("split") + if sample_split not in {"train", "validation", "test"}: + raise DatasetContractError(f"{sample_dir.name} has an invalid dataset split") + if split is not None and sample_split != split: + continue + if metadata.get("mode") != "pump-single": + raise DatasetContractError( + f"{sample_dir.name} is not a five-lane pump-single training sample" + ) + sample_id = str(metadata.get("songId") or "") + if not sample_id or sample_id != sample_dir.name: + raise DatasetContractError(f"{sample_dir.name} has an inconsistent songId") + + audio_path = _safe_sample_asset(sample_dir, metadata.get("audioFile"), "audio") + beatforge_path = _safe_sample_asset( + sample_dir, metadata.get("beatforgeFile"), "BeatForge analysis" + ) + chart_path = _safe_sample_asset(sample_dir, metadata.get("chartFile"), "chart") + expected_audio_hash = str(metadata.get("audioSha256") or "").lower() + if len(expected_audio_hash) != 64: + raise DatasetContractError(f"{sample_dir.name} has no valid audio SHA256") + if verify_audio_hashes and _sha256(audio_path) != expected_audio_hash: + raise DatasetContractError(f"{sample_dir.name} audio SHA256 does not match metadata") + + beatforge = _json_object(beatforge_path) + if beatforge.get("schemaVersion") != TRAINING_FEATURE_SCHEMA_VERSION: + raise DatasetContractError( + f"{sample_dir.name} uses an unsupported BeatForge feature schema" + ) + if str(beatforge.get("audioSha256") or "").lower() != expected_audio_hash: + raise DatasetContractError( + f"{sample_dir.name} BeatForge features refer to a different audio file" + ) + analysis = beatforge.get("analysis") + if not isinstance(analysis, dict): + raise DatasetContractError(f"{sample_dir.name} has no completed analysis payload") + candidates = _value(analysis, "candidate_events", "candidateEvents", default=[]) + if not isinstance(candidates, list) or not candidates: + raise DatasetContractError(f"{sample_dir.name} has no real candidate events") + + try: + chart = ChartDocument.model_validate(_json_object(chart_path)) + except Exception as exc: + raise DatasetContractError(f"{sample_dir.name} chart is invalid: {exc}") from exc + if chart.id != sample_id or chart.mode != "pump-single" or chart.lane_count != 5: + raise DatasetContractError( + f"{sample_dir.name} chart does not match its five-lane sample metadata" + ) + source_difficulty = int(metadata.get("difficulty") or chart.meter) + training_difficulty = min(15, max(1, source_difficulty)) + samples.append( + RealDatasetSample( + sample_id=sample_id, + split=sample_split, + source_group=str(metadata.get("sourceGroup") or "UNKNOWN"), + source_difficulty=source_difficulty, + training_difficulty=training_difficulty, + difficulty_was_clamped=training_difficulty != source_difficulty, + audio_sha256=expected_audio_hash, + metadata=metadata, + beatforge=beatforge, + chart=chart, + sample_dir=sample_dir, + ) + ) + if not samples: + suffix = f" for split {split}" if split else "" + raise DatasetContractError(f"no complete real chart samples found{suffix} in {root}") + return samples + + +@dataclass(frozen=True, slots=True) +class CandidateRecord: + candidate_id: str + time_sec: float + acoustic_sample: int + chart_sample: int + features: tuple[float, ...] + + +def candidate_records(beatforge_payload: dict[str, Any]) -> list[CandidateRecord]: + """Convert a real BeatForge candidate bundle to the stable model feature schema.""" + + analysis_value = beatforge_payload.get("analysis", beatforge_payload) + if not isinstance(analysis_value, dict): + raise DatasetContractError("BeatForge inference payload has no analysis object") + candidates_value = _value(analysis_value, "candidate_events", "candidateEvents", default=[]) + if not isinstance(candidates_value, list) or not candidates_value: + raise DatasetContractError("BeatForge inference requires real candidate events") + sample_rate = int( + _value( + analysis_value, + "original_sample_rate", + "originalSampleRate", + "sample_rate", + "sampleRate", + default=0, + ) + ) + sample_count = int(_value(analysis_value, "sample_count", "sampleCount", default=0)) + if sample_rate <= 0 or sample_count <= 0: + raise DatasetContractError("BeatForge analysis has an invalid sample timeline") + duration_sec = _number( + _value(analysis_value, "duration_sec", "durationSec"), + sample_count / sample_rate, + ) + if duration_sec <= 0: + duration_sec = sample_count / sample_rate + bpm = _number(_value(analysis_value, "bpm", "estimatedBpm"), 120.0) + bpm_confidence = _unit(_value(analysis_value, "bpm_confidence", "bpmConfidence", default=0.0)) + + raw_records: list[tuple[dict[str, Any], str, float, int, int]] = [] + for item in candidates_value: + if not isinstance(item, dict): + raise DatasetContractError("candidate events must be JSON objects") + candidate_id = str(item.get("id") or "") + if not candidate_id: + raise DatasetContractError("candidate event is missing its provenance ID") + acoustic_sample = int( + _value(item, "acoustic_sample", "acousticSample", "sample", default=-1) + ) + chart_sample = int(_value(item, "chart_sample", "chartSample", default=acoustic_sample)) + time_sec = _number(_value(item, "time_sec", "timeSec"), acoustic_sample / sample_rate) + if acoustic_sample < 0 or acoustic_sample >= sample_count or time_sec < 0: + raise DatasetContractError( + f"candidate {candidate_id} lies outside the real audio timeline" + ) + raw_records.append((item, candidate_id, time_sec, acoustic_sample, chart_sample)) + raw_records.sort(key=lambda value: (value[2], value[3], value[1])) + times = [item[2] for item in raw_records] + records: list[CandidateRecord] = [] + for index, (item, candidate_id, time_sec, acoustic_sample, chart_sample) in enumerate( + raw_records + ): + previous_gap = time_sec - times[index - 1] if index else 2.0 + next_gap = times[index + 1] - time_sec if index + 1 < len(times) else 2.0 + status = str(item.get("status") or "uncertain") + lane = str(item.get("lane") or "mix") + source = _mapping(_value(item, "source_evidence", "sourceEvidence", default={})) + semantic = _mapping(_value(item, "semantic_evidence", "semanticEvidence", default={})) + generator = str(item.get("generator") or "analysis") + local_count = bisect.bisect_right(times, time_sec + 0.5) - bisect.bisect_left( + times, time_sec - 0.5 + ) + feature_values = ( + min(1.0, max(0.0, time_sec / duration_sec)), + min(1.0, max(0.0, previous_gap / 2.0)), + min(1.0, max(0.0, next_gap / 2.0)), + _unit(item.get("confidence")), + _unit(_value(item, "grid_confidence", "gridConfidence")), + min( + 1.0, + max( + -1.0, + _number(_value(item, "snap_error_ms", "snapErrorMs")) / 100.0, + ), + ), + float(status == "accepted"), + float(status == "uncertain"), + float(status == "rejected"), + float(lane == "vocals"), + float(lane == "melody"), + float(lane == "drums"), + float(lane == "mix"), + _unit(source.get("vocals")), + _unit(source.get("melody")), + _unit(source.get("drums")), + _unit(source.get("mix")), + _unit(_value(semantic, "lyricAlignment", "lyric_alignment")), + _unit(_value(semantic, "phonemeConfidence", "phoneme_confidence")), + _unit(_value(semantic, "pitchConfidence", "pitch_confidence")), + _unit(_value(semantic, "beatConfidence", "beat_confidence")), + float(generator == "analysis"), + float(generator == "hubert_ctc"), + min(1.0, local_count / 16.0), + min(1.0, max(0.0, bpm / 300.0)), + bpm_confidence, + ) + if len(feature_values) != len(FEATURE_NAMES): + raise AssertionError("candidate feature schema is inconsistent") + records.append( + CandidateRecord( + candidate_id=candidate_id, + time_sec=time_sec, + acoustic_sample=acoustic_sample, + chart_sample=chart_sample, + features=feature_values, + ) + ) + return records + + +@dataclass(frozen=True, slots=True) +class SequenceExample: + sample_id: str + split: SplitName + source_difficulty: int + difficulty: int + audio_sha256: str + records: tuple[CandidateRecord, ...] + lane_targets: tuple[tuple[float, float, float, float, float], ...] + hold_targets: tuple[float, ...] + matched_event_count: int + + +def sequence_example( + sample: RealDatasetSample, *, match_tolerance_ms: float = 80.0 +) -> SequenceExample: + if match_tolerance_ms <= 0: + raise ValueError("match_tolerance_ms must be positive") + records = candidate_records(sample.beatforge) + playable_events = [ + event + for event in sample.chart.events + if any(note.type != "mine" and note.lane < 5 for note in event.notes) + ] + event_times = [event.time_sec for event in playable_events] + tolerance = match_tolerance_ms / 1000.0 + pairs: list[tuple[float, int, int]] = [] + for candidate_index, record in enumerate(records): + left = bisect.bisect_left(event_times, record.time_sec - tolerance) + right = bisect.bisect_right(event_times, record.time_sec + tolerance) + pairs.extend( + (abs(record.time_sec - event_times[event_index]), candidate_index, event_index) + for event_index in range(left, right) + ) + pairs.sort(key=lambda item: (item[0], item[1], item[2])) + matched_candidates: set[int] = set() + matched_events: set[int] = set() + assignments: dict[int, int] = {} + for _distance, candidate_index, event_index in pairs: + if candidate_index in matched_candidates or event_index in matched_events: + continue + matched_candidates.add(candidate_index) + matched_events.add(event_index) + assignments[candidate_index] = event_index + + lane_targets: list[tuple[float, float, float, float, float]] = [] + hold_targets: list[float] = [] + for candidate_index in range(len(records)): + lanes = [0.0] * 5 + hold = 0.0 + event_index = assignments.get(candidate_index) + if event_index is not None: + for note in playable_events[event_index].notes: + if note.type == "mine" or note.lane >= 5: + continue + lanes[note.lane] = 1.0 + if note.type == "hold": + hold = 1.0 + lane_targets.append((lanes[0], lanes[1], lanes[2], lanes[3], lanes[4])) + hold_targets.append(hold) + if not assignments: + raise DatasetContractError( + f"real sample {sample.sample_id} has no chart events within " + f"{match_tolerance_ms:g} ms of its BeatForge candidates" + ) + return SequenceExample( + sample_id=sample.sample_id, + split=sample.split, + source_difficulty=sample.source_difficulty, + difficulty=sample.training_difficulty, + audio_sha256=sample.audio_sha256, + records=tuple(records), + lane_targets=tuple(lane_targets), + hold_targets=tuple(hold_targets), + matched_event_count=len(assignments), + ) + + +@dataclass(frozen=True, slots=True) +class SequenceChunk: + sample_id: str + difficulty: int + features: tuple[tuple[float, ...], ...] + lane_targets: tuple[tuple[float, float, float, float, float], ...] + hold_targets: tuple[float, ...] + + +def sequence_chunks( + examples: list[SequenceExample], *, length: int, stride: int | None = None +) -> list[SequenceChunk]: + if length <= 0: + raise ValueError("sequence length must be positive") + active_stride = stride or length + if active_stride <= 0 or active_stride > length: + raise ValueError("sequence stride must be in the range 1..length") + chunks: list[SequenceChunk] = [] + for example in examples: + for start in range(0, len(example.records), active_stride): + end = min(start + length, len(example.records)) + if end <= start: + continue + chunks.append( + SequenceChunk( + sample_id=example.sample_id, + difficulty=example.difficulty, + features=tuple(record.features for record in example.records[start:end]), + lane_targets=example.lane_targets[start:end], + hold_targets=example.hold_targets[start:end], + ) + ) + if end == len(example.records): + break + if not chunks: + raise DatasetContractError("real training samples produced no candidate sequences") + return chunks + + +@dataclass(frozen=True, slots=True) +class FeatureNormalization: + means: tuple[float, ...] + scales: tuple[float, ...] + + @classmethod + def fit(cls, examples: list[SequenceExample]) -> FeatureNormalization: + rows = [record.features for example in examples for record in example.records] + if not rows: + raise DatasetContractError("cannot normalize an empty real candidate set") + count = len(rows) + means = tuple( + sum(row[index] for row in rows) / count for index in range(len(FEATURE_NAMES)) + ) + variances = tuple( + sum((row[index] - means[index]) ** 2 for row in rows) / count + for index in range(len(FEATURE_NAMES)) + ) + scales = tuple(max(math.sqrt(value), 1e-6) for value in variances) + return cls(means=means, scales=scales) + + def normalize(self, row: tuple[float, ...]) -> tuple[float, ...]: + if len(row) != len(self.means): + raise ValueError("candidate feature count does not match checkpoint normalization") + return tuple( + (value - self.means[index]) / self.scales[index] for index, value in enumerate(row) + ) + + def to_dict(self) -> dict[str, list[float]]: + return {"means": list(self.means), "scales": list(self.scales)} + + @classmethod + def from_dict(cls, value: dict[str, Any]) -> FeatureNormalization: + means = tuple(float(item) for item in value.get("means", [])) + scales = tuple(float(item) for item in value.get("scales", [])) + if len(means) != len(FEATURE_NAMES) or len(scales) != len(FEATURE_NAMES): + raise CheckpointContractError("checkpoint feature normalization is incomplete") + if any(scale <= 0 or not math.isfinite(scale) for scale in scales): + raise CheckpointContractError("checkpoint feature scales are invalid") + return cls(means=means, scales=scales) + + +@dataclass(frozen=True, slots=True) +class TrainingConfig: + epochs: int = 12 + batch_size: int = 8 + learning_rate: float = 3e-4 + weight_decay: float = 1e-4 + gradient_clip: float = 1.0 + match_tolerance_ms: float = 80.0 + sequence_length: int = 512 + sequence_stride: int | None = None + seed: int = 20260721 + device: str = "auto" + validation_split: SplitName | None = "validation" + verify_audio_hashes: bool = True + max_batches_per_epoch: int | None = None + + def __post_init__(self) -> None: + if self.epochs <= 0 or self.batch_size <= 0: + raise ValueError("epochs and batch_size must be positive") + if self.learning_rate <= 0 or self.weight_decay < 0: + raise ValueError("optimizer parameters are invalid") + if self.gradient_clip <= 0 or self.match_tolerance_ms <= 0: + raise ValueError("gradient_clip and match_tolerance_ms must be positive") + if self.sequence_length <= 0: + raise ValueError("sequence_length must be positive") + if ( + self.sequence_stride is not None + and not 0 < self.sequence_stride <= self.sequence_length + ): + raise ValueError("sequence_stride must be in the range 1..sequence_length") + if self.max_batches_per_epoch is not None and self.max_batches_per_epoch <= 0: + raise ValueError("max_batches_per_epoch must be positive when provided") + + def to_dict(self) -> dict[str, Any]: + return asdict(self) + + +def _device(torch_module: Any, requested: str) -> Any: + if requested != "auto": + return torch_module.device(requested) + if hasattr(torch_module.backends, "mps") and torch_module.backends.mps.is_available(): + return torch_module.device("mps") + if torch_module.cuda.is_available(): + return torch_module.device("cuda") + return torch_module.device("cpu") + + +def _normalized_chunks( + chunks: list[SequenceChunk], normalization: FeatureNormalization +) -> list[SequenceChunk]: + return [ + SequenceChunk( + sample_id=chunk.sample_id, + difficulty=chunk.difficulty, + features=tuple(normalization.normalize(row) for row in chunk.features), + lane_targets=chunk.lane_targets, + hold_targets=chunk.hold_targets, + ) + for chunk in chunks + ] + + +class _ChunkDataset: + def __init__(self, chunks: list[SequenceChunk]) -> None: + self.chunks = chunks + + def __len__(self) -> int: + return len(self.chunks) + + def __getitem__(self, index: int) -> SequenceChunk: + return self.chunks[index] + + +def _collate(torch_module: Any, chunks: list[SequenceChunk]) -> dict[str, Any]: + batch_size = len(chunks) + maximum = max(len(chunk.features) for chunk in chunks) + features = torch_module.zeros( + (batch_size, maximum, len(FEATURE_NAMES)), dtype=torch_module.float32 + ) + lane_targets = torch_module.zeros((batch_size, maximum, 5), dtype=torch_module.float32) + hold_targets = torch_module.zeros((batch_size, maximum), dtype=torch_module.float32) + padding_mask = torch_module.ones((batch_size, maximum), dtype=torch_module.bool) + difficulties = torch_module.empty((batch_size,), dtype=torch_module.long) + for index, chunk in enumerate(chunks): + length = len(chunk.features) + features[index, :length] = torch_module.tensor(chunk.features, dtype=torch_module.float32) + lane_targets[index, :length] = torch_module.tensor( + chunk.lane_targets, dtype=torch_module.float32 + ) + hold_targets[index, :length] = torch_module.tensor( + chunk.hold_targets, dtype=torch_module.float32 + ) + padding_mask[index, :length] = False + difficulties[index] = chunk.difficulty + return { + "features": features, + "lane_targets": lane_targets, + "hold_targets": hold_targets, + "padding_mask": padding_mask, + "difficulties": difficulties, + } + + +def _positive_weights(torch_module: Any, chunks: list[SequenceChunk]) -> tuple[Any, Any]: + total = sum(len(chunk.lane_targets) for chunk in chunks) + lane_positive = [ + sum(row[lane] for chunk in chunks for row in chunk.lane_targets) for lane in range(5) + ] + hold_positive = sum(value for chunk in chunks for value in chunk.hold_targets) + if not any(lane_positive): + raise DatasetContractError("real training candidates contain no matched lane targets") + lane_weights = [ + min(50.0, max(1.0, (total - positive) / max(positive, 1.0))) for positive in lane_positive + ] + hold_weight = min(50.0, max(1.0, (total - hold_positive) / max(hold_positive, 1.0))) + return ( + torch_module.tensor(lane_weights, dtype=torch_module.float32), + torch_module.tensor(hold_weight, dtype=torch_module.float32), + ) + + +def _batch_loss( + torch_module: Any, + model: Any, + batch: dict[str, Any], + device: Any, + lane_positive_weights: Any, + hold_positive_weight: Any, +) -> Any: + features = batch["features"].to(device) + lanes = batch["lane_targets"].to(device) + holds = batch["hold_targets"].to(device) + mask = (~batch["padding_mask"]).to(device) + difficulties = batch["difficulties"].to(device) + output = model(features, difficulties, padding_mask=~mask) + lane_loss = torch_module.nn.functional.binary_cross_entropy_with_logits( + output["lane_logits"], + lanes, + pos_weight=lane_positive_weights.to(device), + reduction="none", + ) + hold_loss = torch_module.nn.functional.binary_cross_entropy_with_logits( + output["hold_logits"], + holds, + pos_weight=hold_positive_weight.to(device), + reduction="none", + ) + lane_mask = mask.unsqueeze(-1).expand_as(lane_loss) + lane_value = lane_loss[lane_mask].mean() + hold_value = hold_loss[mask].mean() + return lane_value + 0.45 * hold_value + + +def _evaluate( + torch_module: Any, + model: Any, + loader: Any | None, + device: Any, + lane_positive_weights: Any, + hold_positive_weight: Any, +) -> float | None: + if loader is None: + return None + model.eval() + losses: list[float] = [] + with torch_module.inference_mode(): + for batch in loader: + loss = _batch_loss( + torch_module, + model, + batch, + device, + lane_positive_weights, + hold_positive_weight, + ) + losses.append(float(loss.detach().cpu().item())) + return sum(losses) / len(losses) if losses else None + + +def _dataset_fingerprint(samples: list[RealDatasetSample]) -> str: + identity = [ + { + "sampleId": sample.sample_id, + "split": sample.split, + "audioSha256": sample.audio_sha256, + "chartSha256": sample.metadata.get("chartSha256"), + } + for sample in sorted(samples, key=lambda item: (item.sample_id, item.split)) + ] + payload = json.dumps(identity, sort_keys=True, separators=(",", ":")).encode() + return hashlib.sha256(payload).hexdigest() + + +def _save_checkpoint(path: Path, payload: dict[str, Any], torch_module: Any) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + descriptor, temporary_name = tempfile.mkstemp( + prefix=f".{path.name}.", suffix=".tmp", dir=path.parent + ) + os.close(descriptor) + try: + torch_module.save(payload, temporary_name) + os.replace(temporary_name, path) + except Exception: + Path(temporary_name).unlink(missing_ok=True) + raise + + +@dataclass(frozen=True, slots=True) +class TrainingResult: + checkpoint_path: Path + metadata: dict[str, Any] + + def to_dict(self) -> dict[str, Any]: + return {"checkpointPath": str(self.checkpoint_path), "metadata": self.metadata} + + +def train_chart_transformer( + dataset_dir: str | Path, + checkpoint_path: str | Path, + *, + training: TrainingConfig | None = None, + model_config: ChartTransformerConfig | None = None, +) -> TrainingResult: + """Train and atomically save a local Transformer from complete real triples.""" + + torch_module = require_torch() + active = training or TrainingConfig() + random.seed(active.seed) + torch_module.manual_seed(active.seed) + if torch_module.cuda.is_available(): + torch_module.cuda.manual_seed_all(active.seed) + + train_samples = load_completed_dataset_samples( + dataset_dir, + split="train", + verify_audio_hashes=active.verify_audio_hashes, + ) + validation_samples: list[RealDatasetSample] = [] + if active.validation_split is not None: + try: + validation_samples = load_completed_dataset_samples( + dataset_dir, + split=active.validation_split, + verify_audio_hashes=active.verify_audio_hashes, + ) + except DatasetContractError as exc: + if "no complete real chart samples found" not in str(exc): + raise + train_examples = [ + sequence_example(sample, match_tolerance_ms=active.match_tolerance_ms) + for sample in train_samples + ] + validation_examples = [ + sequence_example(sample, match_tolerance_ms=active.match_tolerance_ms) + for sample in validation_samples + ] + normalization = FeatureNormalization.fit(train_examples) + train_chunks = _normalized_chunks( + sequence_chunks( + train_examples, + length=active.sequence_length, + stride=active.sequence_stride, + ), + normalization, + ) + validation_chunks = ( + _normalized_chunks( + sequence_chunks( + validation_examples, + length=active.sequence_length, + stride=active.sequence_stride, + ), + normalization, + ) + if validation_examples + else [] + ) + configured_model = model_config or ChartTransformerConfig( + input_dim=len(FEATURE_NAMES), max_sequence_length=active.sequence_length + ) + if configured_model.input_dim != len(FEATURE_NAMES): + raise ValueError("model input_dim does not match the candidate feature schema") + if configured_model.max_sequence_length < active.sequence_length: + raise ValueError("model max_sequence_length is smaller than the training sequence length") + + device = _device(torch_module, active.device) + model = ChartTransformer(configured_model).to(device) + optimizer = torch_module.optim.AdamW( + model.parameters(), + lr=active.learning_rate, + weight_decay=active.weight_decay, + ) + generator = torch_module.Generator() + generator.manual_seed(active.seed) + collate = lambda chunks: _collate(torch_module, chunks) # noqa: E731 + train_loader = torch_module.utils.data.DataLoader( + _ChunkDataset(train_chunks), + batch_size=active.batch_size, + shuffle=True, + generator=generator, + num_workers=0, + collate_fn=collate, + ) + validation_loader = ( + torch_module.utils.data.DataLoader( + _ChunkDataset(validation_chunks), + batch_size=active.batch_size, + shuffle=False, + num_workers=0, + collate_fn=collate, + ) + if validation_chunks + else None + ) + lane_positive_weights, hold_positive_weight = _positive_weights(torch_module, train_chunks) + history: list[dict[str, float | int | None]] = [] + best_loss = float("inf") + best_state: dict[str, Any] | None = None + for epoch in range(1, active.epochs + 1): + model.train() + losses: list[float] = [] + for batch_index, batch in enumerate(train_loader): + optimizer.zero_grad(set_to_none=True) + loss = _batch_loss( + torch_module, + model, + batch, + device, + lane_positive_weights, + hold_positive_weight, + ) + loss.backward() + torch_module.nn.utils.clip_grad_norm_(model.parameters(), active.gradient_clip) + optimizer.step() + losses.append(float(loss.detach().cpu().item())) + if ( + active.max_batches_per_epoch is not None + and batch_index + 1 >= active.max_batches_per_epoch + ): + break + train_loss = sum(losses) / len(losses) + validation_loss = _evaluate( + torch_module, + model, + validation_loader, + device, + lane_positive_weights, + hold_positive_weight, + ) + selection_loss = validation_loss if validation_loss is not None else train_loss + history.append( + { + "epoch": epoch, + "trainLoss": train_loss, + "validationLoss": validation_loss, + } + ) + if selection_loss < best_loss: + best_loss = selection_loss + best_state = copy.deepcopy(model.state_dict()) + if best_state is None: + raise RuntimeError("training completed without producing model weights") + model.load_state_dict(best_state) + all_samples = train_samples + validation_samples + all_examples = train_examples + validation_examples + clamped = [sample.sample_id for sample in all_samples if sample.difficulty_was_clamped] + metadata: dict[str, Any] = { + "schemaVersion": CHECKPOINT_SCHEMA_VERSION, + "createdAt": datetime.now(UTC).isoformat(), + "architecture": MODEL_ARCHITECTURE, + "featureSchemaVersion": FEATURE_SCHEMA_VERSION, + "featureNames": list(FEATURE_NAMES), + "trainingSampleSchemaVersion": TRAINING_SAMPLE_SCHEMA_VERSION, + "trainingFeatureSchemaVersion": TRAINING_FEATURE_SCHEMA_VERSION, + "realDataOnly": True, + "datasetFingerprint": _dataset_fingerprint(all_samples), + "sampleCount": len(all_samples), + "trainSampleCount": len(train_samples), + "validationSampleCount": len(validation_samples), + "sequenceCount": len(train_chunks) + len(validation_chunks), + "candidateCount": sum(len(example.records) for example in all_examples), + "matchedEventCount": sum(example.matched_event_count for example in all_examples), + "sampleIds": [sample.sample_id for sample in all_samples], + "audioSha256": [sample.audio_sha256 for sample in all_samples], + "sourceDifficulties": { + sample.sample_id: sample.source_difficulty for sample in all_samples + }, + "difficultyClampedSampleIds": clamped, + "modelConfig": configured_model.to_dict(), + "trainingConfig": active.to_dict(), + "normalization": normalization.to_dict(), + "positiveWeights": { + "lanes": lane_positive_weights.tolist(), + "hold": float(hold_positive_weight.item()), + }, + "history": history, + "bestLoss": best_loss, + "torchVersion": str(torch_module.__version__), + "trainingDevice": str(device), + } + checkpoint = { + "schema_version": CHECKPOINT_SCHEMA_VERSION, + "metadata": metadata, + "model_config": configured_model.to_dict(), + "feature_names": list(FEATURE_NAMES), + "normalization": normalization.to_dict(), + "model_state_dict": model.state_dict(), + } + destination = Path(checkpoint_path).expanduser().resolve() + _save_checkpoint(destination, checkpoint, torch_module) + return TrainingResult(checkpoint_path=destination, metadata=metadata) + + +@dataclass(frozen=True, slots=True) +class CandidatePrediction: + candidate_id: str + time_sec: float + acoustic_sample: int + chart_sample: int + lane_probabilities: tuple[float, float, float, float, float] + hold_probability: float + + def to_dict(self) -> dict[str, Any]: + return { + "candidateId": self.candidate_id, + "timeSec": self.time_sec, + "acousticSample": self.acoustic_sample, + "chartSample": self.chart_sample, + "laneProbabilities": list(self.lane_probabilities), + "holdProbability": self.hold_probability, + } + + +@dataclass(frozen=True, slots=True) +class InferenceResult: + difficulty: int + checkpoint_metadata: dict[str, Any] + predictions: tuple[CandidatePrediction, ...] + + def to_dict(self) -> dict[str, Any]: + return { + "difficulty": self.difficulty, + "checkpoint": self.checkpoint_metadata, + "predictions": [prediction.to_dict() for prediction in self.predictions], + } + + +class LocalChartModel: + """Loaded local checkpoint with deterministic candidate-level inference APIs.""" + + def __init__( + self, + model: Any, + normalization: FeatureNormalization, + metadata: dict[str, Any], + device: Any, + ) -> None: + self.model = model + self.normalization = normalization + self.metadata = metadata + self.device = device + + @classmethod + def load(cls, checkpoint_path: str | Path, *, device: str = "auto") -> LocalChartModel: + torch_module = require_torch() + active_device = _device(torch_module, device) + source = Path(checkpoint_path).expanduser().resolve() + if not source.is_file(): + raise CheckpointContractError(f"chart checkpoint does not exist: {source}") + try: + payload = torch_module.load(source, map_location=active_device, weights_only=True) + except Exception as exc: + raise CheckpointContractError(f"cannot load chart checkpoint: {exc}") from exc + if not isinstance(payload, dict): + raise CheckpointContractError("chart checkpoint payload must be a dictionary") + if payload.get("schema_version") != CHECKPOINT_SCHEMA_VERSION: + raise CheckpointContractError("unsupported chart checkpoint schema") + if payload.get("feature_names") != list(FEATURE_NAMES): + raise CheckpointContractError("checkpoint feature schema does not match this runtime") + metadata = payload.get("metadata") + if not isinstance(metadata, dict) or metadata.get("realDataOnly") is not True: + raise CheckpointContractError("checkpoint lacks real-data training provenance") + model_config_value = payload.get("model_config") + normalization_value = payload.get("normalization") + state = payload.get("model_state_dict") + if not isinstance(model_config_value, dict) or not isinstance(normalization_value, dict): + raise CheckpointContractError("checkpoint configuration is incomplete") + if not isinstance(state, dict): + raise CheckpointContractError("checkpoint contains no model state") + config = ChartTransformerConfig.from_dict(model_config_value) + model = ChartTransformer(config).to(active_device) + try: + model.load_state_dict(state, strict=True) + except Exception as exc: + raise CheckpointContractError(f"checkpoint model state is incompatible: {exc}") from exc + model.eval() + return cls( + model=model, + normalization=FeatureNormalization.from_dict(normalization_value), + metadata=metadata, + device=active_device, + ) + + def predict(self, beatforge_payload: dict[str, Any], *, difficulty: int) -> InferenceResult: + if difficulty < 1 or difficulty > 15: + raise ValueError("difficulty must be between 1 and 15") + torch_module = require_torch() + records = candidate_records(beatforge_payload) + maximum = self.model.config.max_sequence_length + predictions: list[CandidatePrediction] = [] + with torch_module.inference_mode(): + for start in range(0, len(records), maximum): + chunk = records[start : start + maximum] + normalized = [self.normalization.normalize(record.features) for record in chunk] + features = torch_module.tensor( + normalized, dtype=torch_module.float32, device=self.device + ).unsqueeze(0) + difficulties = torch_module.tensor( + [difficulty], dtype=torch_module.long, device=self.device + ) + output = self.model(features, difficulties) + lane_values = torch_module.sigmoid(output["lane_logits"])[0].cpu().tolist() + hold_values = torch_module.sigmoid(output["hold_logits"])[0].cpu().tolist() + for record, lane_row, hold_value in zip( + chunk, lane_values, hold_values, strict=True + ): + predictions.append( + CandidatePrediction( + candidate_id=record.candidate_id, + time_sec=record.time_sec, + acoustic_sample=record.acoustic_sample, + chart_sample=record.chart_sample, + lane_probabilities=( + float(lane_row[0]), + float(lane_row[1]), + float(lane_row[2]), + float(lane_row[3]), + float(lane_row[4]), + ), + hold_probability=float(hold_value), + ) + ) + return InferenceResult( + difficulty=difficulty, + checkpoint_metadata=self.metadata, + predictions=tuple(predictions), + ) + + +def predict_candidate_probabilities( + beatforge_payload: dict[str, Any], + checkpoint_path: str | Path, + *, + difficulty: int, + device: str = "auto", +) -> InferenceResult: + """Convenience API for one-shot, entirely local chart-model inference.""" + + return LocalChartModel.load(checkpoint_path, device=device).predict( + beatforge_payload, difficulty=difficulty + ) diff --git a/apps/api/beatforge_api/chart_engine/library.py b/apps/api/beatforge_api/chart_engine/library.py new file mode 100644 index 0000000..5cda89c --- /dev/null +++ b/apps/api/beatforge_api/chart_engine/library.py @@ -0,0 +1,159 @@ +from __future__ import annotations + +import hashlib +import threading +from dataclasses import dataclass +from functools import lru_cache +from pathlib import Path + +import soundfile as sf + +from .models import ChartDocument, ReferenceChartSummary +from .sm import parse_sm +from .statistics import chart_statistics, corpus_statistics + +_CHART_CACHE_LOCK = threading.RLock() + + +@dataclass(frozen=True, slots=True) +class ReferenceAsset: + id: str + group: str + chart_path: Path + audio_path: Path + + +@lru_cache(maxsize=256) +def _cached_chart( + chart_path: str, + chart_mtime_ns: int, + chart_size: int, + audio_path: str, + audio_mtime_ns: int, + group: str, + chart_id: str, +) -> ChartDocument: + del chart_mtime_ns, chart_size, audio_mtime_ns + chart = parse_sm(chart_path, source_group=group, document_id=chart_id) + try: + audio_duration = float(sf.info(audio_path).duration) + except (RuntimeError, TypeError, ValueError): + audio_duration = chart.duration_sec + updated = chart.model_copy(update={"duration_sec": max(chart.duration_sec, audio_duration)}) + return updated.model_copy(update={"statistics": chart_statistics(updated)}) + + +class ReferenceLibrary: + """Read-only index over the verified SPEED_CLUB/DEVIL/REMIX corpus.""" + + def __init__(self, root: str | Path): + self.root = Path(root).expanduser().resolve() + self._assets = self._scan() + self._by_id = {asset.id: asset for asset in self._assets} + + def _scan(self) -> list[ReferenceAsset]: + if not self.root.is_dir(): + return [] + assets: list[ReferenceAsset] = [] + for chart_path in sorted(self.root.glob("SPEED_*/*/*.sm")): + relative = chart_path.relative_to(self.root).as_posix() + group = chart_path.relative_to(self.root).parts[0] + audio_files = sorted(chart_path.parent.glob("*.mp3")) + if not audio_files: + continue + chart_id = hashlib.sha256(relative.encode("utf-8")).hexdigest()[:20] + assets.append( + ReferenceAsset( + id=chart_id, + group=group, + chart_path=chart_path.resolve(), + audio_path=audio_files[0].resolve(), + ) + ) + return assets + + def __len__(self) -> int: + return len(self._assets) + + def assets(self) -> list[ReferenceAsset]: + return list(self._assets) + + def asset(self, chart_id: str) -> ReferenceAsset: + try: + return self._by_id[chart_id] + except KeyError as exc: + raise KeyError(f"reference chart not found: {chart_id}") from exc + + def chart(self, chart_id: str) -> ChartDocument: + asset = self.asset(chart_id) + chart_stat = asset.chart_path.stat() + audio_stat = asset.audio_path.stat() + # Starlette executes synchronous list/statistics routes in worker + # threads. On a cold start those requests can reach libsndfile/mpg123 at + # the same time, and a few corpus MP3s are not safe to probe + # concurrently. Serialize only cache misses/probes; warm reads remain + # effectively free through the process-local LRU cache. + with _CHART_CACHE_LOCK: + chart = _cached_chart( + str(asset.chart_path), + chart_stat.st_mtime_ns, + chart_stat.st_size, + str(asset.audio_path), + audio_stat.st_mtime_ns, + asset.group, + asset.id, + ) + # ``parse_sm`` records the source it read for local diagnostics. Never + # expose that absolute filesystem path through the reference-chart API. + public_source = asset.chart_path.relative_to(self.root).as_posix() + return chart.model_copy(update={"source_path": public_source}) + + def charts(self, *, mode: str | None = None) -> list[ChartDocument]: + charts = [self.chart(asset.id) for asset in self._assets] + if mode: + charts = [chart for chart in charts if chart.mode == mode] + return charts + + def summaries( + self, + *, + mode: str | None = None, + group: str | None = None, + search: str = "", + ) -> list[ReferenceChartSummary]: + needle = search.strip().casefold() + output: list[ReferenceChartSummary] = [] + for asset in self._assets: + if group and asset.group != group: + continue + chart = self.chart(asset.id) + if mode and chart.mode != mode: + continue + if needle and needle not in f"{chart.title} {asset.group} {chart.music}".casefold(): + continue + stats = chart.statistics or chart_statistics(chart) + output.append( + ReferenceChartSummary( + id=asset.id, + title=chart.title, + group=asset.group, + mode=chart.mode, + lane_count=chart.lane_count, + difficulty=chart.difficulty, + meter=chart.meter, + bpm=chart.bpm, + bpm_max=max(point.bpm for point in chart.tempo_map), + offset_sec=chart.offset_sec, + duration_sec=chart.duration_sec, + note_count=stats.note_count, + event_count=stats.event_count, + nps_average=stats.nps_average, + nps_peak=stats.nps_peak, + audio_url=f"/api/chart-engine/reference-charts/{asset.id}/audio", + chart_url=f"/api/chart-engine/reference-charts/{asset.id}", + ) + ) + return output + + def statistics(self): + return corpus_statistics(self.charts()) diff --git a/apps/api/beatforge_api/chart_engine/model.py b/apps/api/beatforge_api/chart_engine/model.py new file mode 100644 index 0000000..165d61a --- /dev/null +++ b/apps/api/beatforge_api/chart_engine/model.py @@ -0,0 +1,156 @@ +from __future__ import annotations + +from dataclasses import asdict, dataclass +from typing import Any + +try: + import torch + from torch import nn +except ImportError: # pragma: no cover - exercised by installs without chart-ml + torch = None # type: ignore[assignment] + nn = None # type: ignore[assignment] + + +MODEL_ARCHITECTURE = "beatforge.chart-transformer.encoder.v1" + + +class TorchUnavailableError(RuntimeError): + """Raised when local chart inference is requested without PyTorch installed.""" + + +def torch_available() -> bool: + return torch is not None and nn is not None + + +def require_torch() -> Any: + if not torch_available(): + raise TorchUnavailableError( + "PyTorch is required for the chart sequence model; install beatforge-api[chart-ml]." + ) + return torch + + +@dataclass(frozen=True, slots=True) +class ChartTransformerConfig: + input_dim: int + d_model: int = 96 + nhead: int = 4 + num_layers: int = 3 + dim_feedforward: int = 256 + dropout: float = 0.1 + max_sequence_length: int = 512 + lane_count: int = 5 + maximum_difficulty: int = 15 + + def __post_init__(self) -> None: + if self.input_dim <= 0: + raise ValueError("input_dim must be positive") + if self.d_model <= 0 or self.d_model % self.nhead: + raise ValueError("d_model must be positive and divisible by nhead") + if self.num_layers <= 0 or self.dim_feedforward <= 0: + raise ValueError("Transformer layer sizes must be positive") + if not 0 <= self.dropout < 1: + raise ValueError("dropout must be in [0, 1)") + if self.max_sequence_length <= 0: + raise ValueError("max_sequence_length must be positive") + if self.lane_count != 5: + raise ValueError("the SPEED single-panel model requires exactly five lanes") + if self.maximum_difficulty != 15: + raise ValueError("the chart engine difficulty contract is fixed to 1-15") + + def to_dict(self) -> dict[str, Any]: + return asdict(self) + + @classmethod + def from_dict(cls, value: dict[str, Any]) -> ChartTransformerConfig: + return cls(**value) + + +if nn is not None: + + class ChartTransformer(nn.Module): + """Transformer encoder over ordered BeatForge candidate events. + + Five independent lane logits allow jumps, while a separate event-level + hold logit estimates whether at least one predicted lane should start a hold. + Difficulty is a learned sequence condition and is always in the public 1-15 + range. + """ + + def __init__(self, config: ChartTransformerConfig) -> None: + super().__init__() + self.config = config + self.input_projection = nn.Linear(config.input_dim, config.d_model) + self.position_embedding = nn.Embedding(config.max_sequence_length, config.d_model) + self.difficulty_embedding = nn.Embedding(config.maximum_difficulty + 1, config.d_model) + layer = nn.TransformerEncoderLayer( + d_model=config.d_model, + nhead=config.nhead, + dim_feedforward=config.dim_feedforward, + dropout=config.dropout, + activation="gelu", + batch_first=True, + norm_first=True, + ) + self.encoder = nn.TransformerEncoder( + layer, + num_layers=config.num_layers, + norm=nn.LayerNorm(config.d_model), + enable_nested_tensor=False, + ) + self.output_norm = nn.LayerNorm(config.d_model) + self.lane_head = nn.Linear(config.d_model, config.lane_count) + self.hold_head = nn.Linear(config.d_model, 1) + self.reset_parameters() + + def reset_parameters(self) -> None: + nn.init.normal_(self.position_embedding.weight, std=0.02) + nn.init.normal_(self.difficulty_embedding.weight, std=0.02) + nn.init.xavier_uniform_(self.input_projection.weight) + nn.init.zeros_(self.input_projection.bias) + nn.init.xavier_uniform_(self.lane_head.weight) + nn.init.zeros_(self.lane_head.bias) + nn.init.xavier_uniform_(self.hold_head.weight) + nn.init.zeros_(self.hold_head.bias) + + def forward( + self, + features: Any, + difficulties: Any, + padding_mask: Any | None = None, + ) -> dict[str, Any]: + if features.ndim != 3: + raise ValueError("features must have shape [batch, sequence, feature]") + batch_size, sequence_length, feature_count = features.shape + if feature_count != self.config.input_dim: + raise ValueError( + f"expected {self.config.input_dim} features, received {feature_count}" + ) + if sequence_length > self.config.max_sequence_length: + raise ValueError( + "sequence length exceeds the configured positional embedding limit" + ) + if difficulties.shape != (batch_size,): + raise ValueError("difficulties must have shape [batch]") + if bool( + ((difficulties < 1) | (difficulties > self.config.maximum_difficulty)).any().item() + ): + raise ValueError("difficulty must be between 1 and 15") + + positions = torch.arange(sequence_length, device=features.device) + positions = positions.unsqueeze(0).expand(batch_size, sequence_length) + encoded = self.input_projection(features) + encoded = encoded + self.position_embedding(positions) + encoded = encoded + self.difficulty_embedding(difficulties).unsqueeze(1) + encoded = self.encoder(encoded, src_key_padding_mask=padding_mask) + encoded = self.output_norm(encoded) + return { + "lane_logits": self.lane_head(encoded), + "hold_logits": self.hold_head(encoded).squeeze(-1), + } + +else: + + class ChartTransformer: # pragma: no cover - only defined without PyTorch + def __init__(self, _config: ChartTransformerConfig) -> None: + require_torch() diff --git a/apps/api/beatforge_api/chart_engine/models.py b/apps/api/beatforge_api/chart_engine/models.py new file mode 100644 index 0000000..9a93169 --- /dev/null +++ b/apps/api/beatforge_api/chart_engine/models.py @@ -0,0 +1,178 @@ +from __future__ import annotations + +from typing import Any, Literal + +from pydantic import Field, model_validator + +from ..schemas import ApiModel + +ChartMode = Literal["pump-single", "pump-double"] +NoteType = Literal["tap", "hold", "mine"] +IssueSeverity = Literal["info", "warning", "error"] + + +class TempoPoint(ApiModel): + beat: float = Field(ge=0) + # Some real-world SM files use extreme BPM values to encode stops/warps. + bpm: float = Field(gt=0, le=10_000_000) + time_sec: float + + +class ChartNote(ApiModel): + lane: int = Field(ge=0, le=9) + type: NoteType = "tap" + end_time_sec: float | None = None + end_beat: float | None = None + source: str = "sm" + confidence: float = Field(default=1.0, ge=0, le=1) + foot: Literal["left", "right"] | None = None + + @model_validator(mode="after") + def validate_hold(self) -> ChartNote: + if self.type == "hold": + if self.end_time_sec is None or self.end_beat is None: + raise ValueError("hold notes require end_time_sec and end_beat") + return self + + +class ChartEvent(ApiModel): + time_sec: float + beat: float = Field(ge=0) + measure: int = Field(ge=0) + subdivision: int = Field(default=4, ge=1) + row_index: int | None = Field(default=None, ge=0) + notes: list[ChartNote] = Field(min_length=1) + source_event_id: str | None = None + # A quantized chart row can merge several BeatForge inputs. Keep the + # legacy primary id above for existing consumers, while retaining the + # complete provenance needed to audit anchor coverage. + source_event_ids: list[str] = Field(default_factory=list) + source_hit_point_ids: list[str] = Field(default_factory=list) + # 0 = optional/model-selected, 1 = BeatForge rhythm marker (accepted + # candidates and non-rejected aligned vocal morae), 2 = confirmed hit point. + anchor_priority: int = Field(default=0, ge=0, le=2) + pattern: str | None = None + + +class ChartStatistics(ApiModel): + note_count: int = Field(ge=0) + event_count: int = Field(ge=0) + hold_count: int = Field(ge=0) + jump_count: int = Field(ge=0) + mine_count: int = Field(ge=0) + duration_sec: float = Field(ge=0) + nps_average: float = Field(ge=0) + nps_peak: float = Field(ge=0) + single_ratio: float = Field(ge=0, le=1) + jump_ratio: float = Field(ge=0, le=1) + hold_ratio: float = Field(ge=0, le=1) + lane_counts: list[int] + measure_densities: list[float] + same_foot_runs: list[int] = Field(default_factory=list) + foot_switch_ratio: float = Field(default=0, ge=0, le=1) + small_spin_count: int = Field(default=0, ge=0) + big_spin_count: int = Field(default=0, ge=0) + + +class ValidationIssue(ApiModel): + code: str + severity: IssueSeverity + message: str + time_sec: float | None = None + beat: float | None = None + penalty: float = Field(default=0, ge=0) + + +class ValidationResult(ApiModel): + valid: bool + score: float = Field(ge=0, le=100) + issues: list[ValidationIssue] + metrics: dict[str, Any] = Field(default_factory=dict) + + +class ChartDocument(ApiModel): + id: str + title: str + artist: str = "" + music: str = "" + source_group: str | None = None + source_path: str | None = None + mode: ChartMode = "pump-single" + lane_count: int = Field(default=5, ge=5, le=10) + difficulty: str = "Hard" + meter: int = Field(default=1, ge=1, le=99) + bpm: float = Field(gt=0, le=500) + offset_sec: float = 0 + duration_sec: float = Field(default=0, ge=0) + measure_count: int = Field(default=0, ge=0) + tempo_map: list[TempoPoint] = Field(min_length=1) + events: list[ChartEvent] + statistics: ChartStatistics | None = None + validation: ValidationResult | None = None + optimization: dict[str, int] | None = None + model_provenance: dict[str, Any] | None = None + generator: str = "sm_parser" + generator_version: str = "1.0" + seed: int | None = None + spin_enabled: bool = False + + @model_validator(mode="after") + def validate_lanes(self) -> ChartDocument: + if self.mode == "pump-single" and self.lane_count != 5: + raise ValueError("pump-single charts require five lanes") + if self.mode == "pump-double" and self.lane_count != 10: + raise ValueError("pump-double charts require ten lanes") + for event in self.events: + if any(note.lane >= self.lane_count for note in event.notes): + raise ValueError("note lane exceeds chart lane count") + return self + + +class ReferenceChartSummary(ApiModel): + id: str + title: str + group: str + mode: ChartMode + lane_count: int + difficulty: str + meter: int + bpm: float + bpm_max: float + offset_sec: float + duration_sec: float + note_count: int + event_count: int + nps_average: float + nps_peak: float + audio_url: str + chart_url: str + + +class CorpusStatistics(ApiModel): + chart_count: int + song_count: int + single_chart_count: int + single_song_count: int + double_chart_count: int + double_song_count: int + difficulty_min: int + difficulty_max: int + total_notes: int + total_duration_sec: float + average_nps: float + groups: dict[str, int] + lane_transition_probabilities: list[list[float]] + meter_profiles: dict[str, dict[str, float]] + + +class GenerateChartRequest(ApiModel): + difficulty: int = Field(default=8, ge=1, le=15) + enable_spin: bool = False + use_local_model: bool = True + seed: int | None = None + + +class ChartGenerationResponse(ApiModel): + generation_id: str + chart: ChartDocument + reference_corpus: dict[str, Any] diff --git a/apps/api/beatforge_api/chart_engine/optimizer.py b/apps/api/beatforge_api/chart_engine/optimizer.py new file mode 100644 index 0000000..d8656ee --- /dev/null +++ b/apps/api/beatforge_api/chart_engine/optimizer.py @@ -0,0 +1,240 @@ +from __future__ import annotations + +from collections import deque +from dataclasses import dataclass +from typing import Any + +from .footwork import LaneEvidenceKey, LaneProbabilities, repair_no_spin_footwork +from .models import ChartEvent +from .rhythm_policy import density_note_limit + + +@dataclass(frozen=True, slots=True) +class OptimizationReport: + input_events: int + output_events: int + duplicate_notes_removed: int + simultaneous_notes_removed: int + density_events_removed: int + footwork_lanes_reassigned: int + footwork_feet_assigned: int + footwork_segments_repaired: int + + +@dataclass(slots=True) +class _SelectedEvent: + event: ChartEvent + playable_notes: int + anchor_priority: int + confidence: float + removed: bool = False + + +def _field_has_value(event: ChartEvent, name: str) -> bool: + value: Any = getattr(event, name, None) + if isinstance(value, bool): + return value + if isinstance(value, str): + return bool(value) + if value is None: + return False + try: + return bool(len(value)) + except TypeError: + return bool(value) + + +def _anchor_priority(event: ChartEvent) -> int: + """Return the strongest available rhythm-anchor priority. + + New charts carry ``anchor_priority`` explicitly. The remaining checks keep + optimization safe for charts produced before that field existed and for + partially migrated fixtures. + """ + + raw_priority = getattr(event, "anchor_priority", None) + has_explicit_priority = raw_priority is not None + try: + priority = min(2, max(0, int(raw_priority or 0))) + except (TypeError, ValueError): + priority = 0 + if _field_has_value(event, "source_hit_point_ids") or any( + note.source == "hit_point" for note in event.notes + ): + priority = 2 + if not has_explicit_priority and ( + _field_has_value(event, "protected_source_ids") + or _field_has_value(event, "source_event_ids") + or _field_has_value(event, "is_protected") + ): + priority = max(priority, 1) + return priority + + +def _playable_notes(event: ChartEvent) -> list[Any]: + return [note for note in event.notes if note.type != "mine"] + + +def _event_confidence(event: ChartEvent) -> float: + notes = _playable_notes(event) or event.notes + return sum(note.confidence for note in notes) / max(len(notes), 1) + + +def _downgrade_jump(event: ChartEvent) -> tuple[ChartEvent, int]: + """Reduce a jump to its strongest playable note without losing its timing.""" + + playable = _playable_notes(event) + if len(playable) <= 1: + return event, 0 + strongest = max(playable, key=lambda note: note.confidence) + notes = [ + note + for note in event.notes + if note.type == "mine" or note is strongest + ] + return event.model_copy(update={"notes": notes}), len(playable) - 1 + + +def _limit_disjoint_jump_transitions( + events: list[ChartEvent], difficulty: int +) -> tuple[list[ChartEvent], int]: + """Remove forced jump-to-jump repositioning from lower-level charts. + + Two nearby jumps that share no panel require both feet to leave and land on + a different pair. At Lv.10 and below, keep both rhythm rows and downgrade + the later row to its strongest panel. Shared-pivot pairs and isolated jumps + remain intact. + """ + + if difficulty > 10: + return events, 0 + output: list[ChartEvent] = [] + removed_notes = 0 + for event in events: + previous = output[-1] if output else None + previous_playable = _playable_notes(previous) if previous is not None else [] + playable = _playable_notes(event) + if ( + previous is not None + and len(previous_playable) > 1 + and len(playable) > 1 + and event.beat - previous.beat <= 1.0 + 1e-9 + and {note.lane for note in previous_playable}.isdisjoint( + note.lane for note in playable + ) + ): + event, removed = _downgrade_jump(event) + removed_notes += removed + output.append(event) + return output, removed_notes + + +def optimize_events( + events: list[ChartEvent], + difficulty: int, + *, + bpm: float | None = None, + lane_probabilities: dict[LaneEvidenceKey, LaneProbabilities] | None = None, +) -> tuple[list[ChartEvent], OptimizationReport]: + """Apply deterministic panel and density constraints before validation.""" + + normalized: list[ChartEvent] = [] + duplicate_notes_removed = 0 + simultaneous_notes_removed = 0 + for event in sorted(events, key=lambda item: (item.time_sec, item.beat)): + by_lane = {} + for note in sorted(event.notes, key=lambda item: item.confidence, reverse=True): + if note.lane in by_lane: + duplicate_notes_removed += 1 + continue + by_lane[note.lane] = note + notes = list(by_lane.values()) + if len(notes) > 2: + simultaneous_notes_removed += len(notes) - 2 + notes = notes[:2] + if notes: + normalized.append(event.model_copy(update={"notes": notes})) + + normalized, jump_notes_removed = _limit_disjoint_jump_transitions( + normalized, difficulty + ) + simultaneous_notes_removed += jump_notes_removed + + limit = density_note_limit(difficulty, bpm=bpm) + recent: deque[_SelectedEvent] = deque() + selected: list[_SelectedEvent] = [] + density_removed = 0 + for event in normalized: + while recent and event.time_sec - recent[0].event.time_sec >= 2.0: + recent.popleft() + + count = len(_playable_notes(event)) + current_notes = sum( + item.playable_notes for item in recent if not item.removed + ) + if current_notes + count > limit and count > 1: + event, removed_notes = _downgrade_jump(event) + count -= removed_notes + simultaneous_notes_removed += removed_notes + + priority = _anchor_priority(event) + if current_notes + count > limit and priority == 0: + density_removed += 1 + continue + + while current_notes + count > limit: + replaceable = [ + item + for item in recent + if not item.removed and item.anchor_priority == 0 + ] + if not replaceable: + # BeatForge rhythm markers (including aligned vocal morae) and + # confirmed hit points are anchors. Keep every timing row and + # let the validator describe any unavoidable density excess. + break + victim = min( + replaceable, + key=lambda item: ( + item.anchor_priority, + item.confidence, + -item.playable_notes, + item.event.time_sec, + ), + ) + if victim.anchor_priority == 0 and victim.playable_notes > 1: + downgraded, removed_notes = _downgrade_jump(victim.event) + victim.event = downgraded + victim.playable_notes -= removed_notes + simultaneous_notes_removed += removed_notes + else: + victim.removed = True + density_removed += 1 + current_notes = sum( + item.playable_notes for item in recent if not item.removed + ) + + item = _SelectedEvent( + event=event, + playable_notes=count, + anchor_priority=priority, + confidence=_event_confidence(event), + ) + selected.append(item) + recent.append(item) + + output = [item.event for item in selected if not item.removed] + output, footwork = repair_no_spin_footwork( + output, lane_probabilities=lane_probabilities + ) + simultaneous_notes_removed += footwork.notes_removed + return output, OptimizationReport( + input_events=len(events), + output_events=len(output), + duplicate_notes_removed=duplicate_notes_removed, + simultaneous_notes_removed=simultaneous_notes_removed, + density_events_removed=density_removed, + footwork_lanes_reassigned=footwork.lanes_reassigned, + footwork_feet_assigned=footwork.feet_assigned, + footwork_segments_repaired=footwork.segments_repaired, + ) diff --git a/apps/api/beatforge_api/chart_engine/rhythm_policy.py b/apps/api/beatforge_api/chart_engine/rhythm_policy.py new file mode 100644 index 0000000..ca22149 --- /dev/null +++ b/apps/api/beatforge_api/chart_engine/rhythm_policy.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +import math + + +def density_note_limit( + difficulty: int, + *, + bpm: float | None = None, + window_sec: float = 2.0, +) -> int: + """Return a shared integer note cap for a half-open density window. + + Lv.8+ supports sustained 1/16 rows. The BPM-aware floor keeps that legal + rhythm from being rejected simply because a song is faster, while jumps + still count as two notes and 1/24 pressure can exceed the cap. + """ + + level = min(max(int(difficulty), 1), 15) + learned_cap = math.floor((2.4 + level * 0.58) * window_sec) + if level < 8 or bpm is None or not math.isfinite(bpm) or bpm <= 0: + return max(1, learned_cap) + sixteenth_cap = math.ceil(window_sec * bpm / 15.0 - 1e-9) + return max(1, learned_cap, sixteenth_cap) + + +def density_limit_nps(difficulty: int, *, bpm: float | None = None) -> float: + return density_note_limit(difficulty, bpm=bpm) / 2.0 + + +def maximum_subdivision(difficulty: int) -> int: + return 16 if int(difficulty) <= 10 else 24 diff --git a/apps/api/beatforge_api/chart_engine/sm.py b/apps/api/beatforge_api/chart_engine/sm.py new file mode 100644 index 0000000..581a72d --- /dev/null +++ b/apps/api/beatforge_api/chart_engine/sm.py @@ -0,0 +1,325 @@ +from __future__ import annotations + +import hashlib +import math +import re +from pathlib import Path +from typing import Any + +from .models import ChartDocument, ChartEvent, ChartNote +from .timing import TempoTimeline + +_HEADER = re.compile(r"^\s*#([A-Z0-9_]+)\s*:\s*(.*?)\s*;?\s*$", re.IGNORECASE) +_NOTES_START = re.compile(r"#NOTES\s*:", re.IGNORECASE) +_VALID_ROW = re.compile(r"^[0-4MFKL]+$", re.IGNORECASE) + + +def _read_text(path: Path) -> str: + raw = path.read_bytes() + for encoding in ("utf-8-sig", "gb18030", "shift_jis"): + try: + return raw.decode(encoding).replace("\r\n", "\n").replace("\r", "\n") + except UnicodeDecodeError: + continue + return raw.decode("utf-8", errors="replace").replace("\r", "\n") + + +def _header_tags(text: str) -> dict[str, str]: + match = _NOTES_START.search(text) + header = text[: match.start()] if match else text + tags: dict[str, str] = {} + for line in header.splitlines(): + parsed = _HEADER.match(line) + if parsed: + tags[parsed.group(1).upper()] = parsed.group(2).strip().rstrip(";").strip() + return tags + + +def _number(value: str | None, fallback: float = 0.0) -> float: + try: + return float((value or "").strip()) + except ValueError: + return fallback + + +def _tempo_changes(value: str) -> list[tuple[float, float]]: + changes: list[tuple[float, float]] = [] + for item in value.rstrip(";, ").split(","): + if "=" not in item: + continue + beat_value, bpm_value = item.split("=", 1) + try: + beat = float(beat_value.strip()) + bpm = float(bpm_value.strip().rstrip(";")) + except ValueError: + continue + if beat >= 0 and 0 < bpm <= 10_000_000: + changes.append((beat, bpm)) + if not changes: + raise ValueError("SM chart has no valid #BPMS entries") + return changes + + +def _notes_blocks(text: str) -> list[str]: + blocks: list[str] = [] + for match in _NOTES_START.finditer(text): + start = match.end() + end = text.find(";", start) + if end < 0: + end = len(text) + blocks.append(text[start:end]) + return blocks + + +def _clean_notes_block(block: str) -> str: + lines = [] + for raw_line in block.splitlines(): + line = raw_line.split("//", 1)[0].strip() + if line: + lines.append(line) + return "\n".join(lines) + + +def _parse_block_metadata(block: str) -> tuple[str, str, str, int, str]: + parts = _clean_notes_block(block).split(":", 5) + if len(parts) != 6: + raise ValueError("SM #NOTES block is missing metadata fields") + mode = parts[0].strip().lower() + description = parts[1].strip() + difficulty = parts[2].strip() or "Hard" + try: + meter = max(1, int(float(parts[3].strip()))) + except ValueError: + meter = 1 + return mode, description, difficulty, meter, parts[5] + + +def _event_note_count(events: list[dict[str, Any]]) -> int: + return sum(len(event["notes"]) for event in events) + + +def parse_sm( + path: str | Path, + *, + chart_index: int = 0, + source_group: str | None = None, + document_id: str | None = None, +) -> ChartDocument: + """Parse a real StepMania pump chart into absolute timestamped events.""" + + source = Path(path).expanduser().resolve() + text = _read_text(source) + tags = _header_tags(text) + changes = _tempo_changes(tags.get("BPMS", "")) + offset = _number(tags.get("OFFSET"), 0.0) + timeline = TempoTimeline(changes, offset) + blocks = _notes_blocks(text) + if not blocks: + raise ValueError("SM file contains no #NOTES block") + if chart_index < 0 or chart_index >= len(blocks): + raise IndexError("SM chart index is out of range") + mode, _description, difficulty, meter, note_data = _parse_block_metadata(blocks[chart_index]) + + measures: list[list[str]] = [] + lane_count = 5 if mode == "pump-single" else 10 if mode == "pump-double" else 0 + for raw_measure in note_data.split(","): + rows: list[str] = [] + for raw_line in raw_measure.splitlines(): + line = raw_line.split("//", 1)[0].strip().upper() + if not line or not _VALID_ROW.fullmatch(line): + continue + if lane_count == 0: + lane_count = len(line) + if len(line) != lane_count: + raise ValueError( + f"inconsistent SM row width in {source.name}: " + f"expected {lane_count}, got {len(line)}" + ) + rows.append(line) + measures.append(rows) + if lane_count not in {5, 10}: + raise ValueError(f"unsupported SM lane count: {lane_count}") + normalized_mode = "pump-single" if lane_count == 5 else "pump-double" + + event_rows: list[dict[str, Any]] = [] + active_holds: dict[int, dict[str, Any]] = {} + for measure_index, rows in enumerate(measures): + row_count = len(rows) + if row_count == 0: + continue + for row_index, row in enumerate(rows): + beat = measure_index * 4.0 + row_index * 4.0 / row_count + time_sec = timeline.beat_to_time(beat) + row_notes: list[dict[str, Any]] = [] + for lane, symbol in enumerate(row): + if symbol == "0": + continue + if symbol in {"2", "4"}: + previous = active_holds.get(lane) + if previous is not None: + previous["type"] = "tap" + previous.pop("end_time_sec", None) + previous.pop("end_beat", None) + note = { + "lane": lane, + "type": "hold", + "end_time_sec": None, + "end_beat": None, + "source": "sm", + "confidence": 1.0, + } + row_notes.append(note) + active_holds[lane] = note + elif symbol == "3": + active = active_holds.pop(lane, None) + if active is not None: + active["end_time_sec"] = max(time_sec, timeline.beat_to_time(beat)) + active["end_beat"] = beat + elif symbol == "M": + row_notes.append( + {"lane": lane, "type": "mine", "source": "sm", "confidence": 1.0} + ) + else: + row_notes.append( + {"lane": lane, "type": "tap", "source": "sm", "confidence": 1.0} + ) + if row_notes: + event_rows.append( + { + "time_sec": time_sec, + "beat": beat, + "measure": measure_index, + "subdivision": row_count, + "row_index": row_index, + "notes": row_notes, + } + ) + + # Corrupt/unclosed hold starts should remain visible and playable as taps. + for active in active_holds.values(): + active["type"] = "tap" + active.pop("end_time_sec", None) + active.pop("end_beat", None) + + events: list[ChartEvent] = [] + for raw in event_rows: + notes = [ChartNote.model_validate(note) for note in raw.pop("notes")] + events.append(ChartEvent(**raw, notes=notes)) + events.sort(key=lambda event: (event.time_sec, event.beat)) + duration = max( + ( + max( + [event.time_sec] + + [note.end_time_sec for note in event.notes if note.end_time_sec is not None] + ) + for event in events + ), + default=0.0, + ) + digest = hashlib.sha256(source.read_bytes()).hexdigest()[:20] + chart = ChartDocument( + id=document_id or digest, + title=tags.get("TITLE", source.stem), + artist=tags.get("ARTIST", ""), + music=tags.get("MUSIC", ""), + source_group=source_group, + source_path=str(source), + mode=normalized_mode, + lane_count=lane_count, + difficulty=difficulty, + meter=meter, + bpm=timeline.primary_bpm, + offset_sec=offset, + duration_sec=max(0.0, duration), + measure_count=len(measures), + tempo_map=timeline.points(), + events=events, + generator="sm_parser", + ) + from .statistics import chart_statistics + + return chart.model_copy(update={"statistics": chart_statistics(chart)}) + + +def _safe_tag(value: str) -> str: + return value.replace(";", "").replace("\r", " ").replace("\n", " ").strip() + + +def _put_symbol(row: list[str], lane: int, symbol: str) -> None: + priority = {"0": 0, "M": 1, "1": 2, "3": 3, "2": 4} + if priority.get(symbol, 0) >= priority.get(row[lane], 0): + row[lane] = symbol + + +def export_sm(chart: ChartDocument, *, rows_per_measure: int | None = None) -> str: + """Export a chart as a deterministic StepMania SM document.""" + + if rows_per_measure is None: + rows_per_measure = 4 + for subdivision in {event.subdivision for event in chart.events}: + rows_per_measure = math.lcm(rows_per_measure, subdivision) + if rows_per_measure < 4 or rows_per_measure % 4: + raise ValueError("rows_per_measure must be a multiple of four") + last_beat = max( + ( + max([event.beat] + [note.end_beat for note in event.notes if note.end_beat is not None]) + for event in chart.events + ), + default=0.0, + ) + measure_count = max(1, int(math.floor(last_beat / 4.0)) + 1) + grid = [ + [["0"] * chart.lane_count for _ in range(rows_per_measure)] for _ in range(measure_count) + ] + + def location(beat: float) -> tuple[int, int]: + absolute_row = max(0, int(round(beat * rows_per_measure / 4.0))) + measure, row = divmod(absolute_row, rows_per_measure) + while measure >= len(grid): + grid.append([["0"] * chart.lane_count for _ in range(rows_per_measure)]) + return measure, row + + for event in chart.events: + measure, row = location(event.beat) + for note in event.notes: + if note.type == "mine": + _put_symbol(grid[measure][row], note.lane, "M") + elif note.type == "hold" and note.end_beat is not None: + _put_symbol(grid[measure][row], note.lane, "2") + end_measure, end_row = location(note.end_beat) + _put_symbol(grid[end_measure][end_row], note.lane, "3") + else: + _put_symbol(grid[measure][row], note.lane, "1") + + bpms = ",".join(f"{point.beat:.6f}={point.bpm:.6f}" for point in chart.tempo_map) + display_values = [point.bpm for point in chart.tempo_map if point.bpm <= 500] + display_min = min(display_values, default=chart.bpm) + display_max = max(display_values, default=chart.bpm) + display = ( + f"{display_min:.3f}" + if math.isclose(display_min, display_max) + else f"{display_min:.3f}:{display_max:.3f}" + ) + measures = [] + for index, rows in enumerate(grid): + body = "\n".join("".join(row) for row in rows) + suffix = "," if index < len(grid) - 1 else ";" + measures.append(f" // measure {index + 1}\n{body}\n{suffix}") + notes_body = "\n".join(measures) + radar = "0.000,0.000,0.000,0.000,0.000" + return ( + f"#TITLE:{_safe_tag(chart.title)};\n" + f"#SUBTITLE:;\n" + f"#ARTIST:{_safe_tag(chart.artist)};\n" + f"#MUSIC:{_safe_tag(chart.music)};\n" + f"#DISPLAYBPM:{display};\n" + f"#OFFSET:{chart.offset_sec:.6f};\n" + f"#BPMS:{bpms};\n" + f"#NOTES:\n" + f" {chart.mode}:\n" + f" BeatForge AI:\n" + f" {chart.difficulty}:\n" + f" {chart.meter}:\n" + f" {radar}:\n" + f"{notes_body}\n" + ) diff --git a/apps/api/beatforge_api/chart_engine/statistics.py b/apps/api/beatforge_api/chart_engine/statistics.py new file mode 100644 index 0000000..20aed7c --- /dev/null +++ b/apps/api/beatforge_api/chart_engine/statistics.py @@ -0,0 +1,197 @@ +from __future__ import annotations + +from collections import Counter, defaultdict +from collections.abc import Iterable + +from .models import ChartDocument, ChartStatistics, CorpusStatistics + +_BIG_SPIN = (3, 2, 4, 2, 1, 2, 0, 2) +_SMALL_SPIN = (3, 2, 4) * 3 + + +def _peak_nps(times: list[float], window_sec: float = 1.0) -> float: + if not times: + return 0.0 + ordered = sorted(times) + left = 0 + peak = 0 + for right, time_sec in enumerate(ordered): + while time_sec - ordered[left] > window_sec: + left += 1 + peak = max(peak, right - left + 1) + return peak / window_sec + + +def _primary_lane_sequence(chart: ChartDocument) -> list[int]: + return [ + event.notes[0].lane + for event in chart.events + if len(event.notes) == 1 and event.notes[0].type != "mine" + ] + + +def _adjacent_single_lane_transitions(chart: ChartDocument) -> Iterable[tuple[int, int]]: + """Yield only transitions that are adjacent in the original chart. + + Projecting a chart to single rows first used to bridge across a removed + jump. That taught lower-level generation transitions which only existed + because a two-foot reset row had been discarded. + """ + + for previous, current in zip(chart.events, chart.events[1:], strict=False): + previous_playable = [note for note in previous.notes if note.type != "mine"] + current_playable = [note for note in current.notes if note.type != "mine"] + if len(previous_playable) == len(current_playable) == 1: + yield previous_playable[0].lane, current_playable[0].lane + + +def _pattern_count(sequence: list[int], pattern: tuple[int, ...]) -> int: + if len(sequence) < len(pattern): + return 0 + return sum( + tuple(sequence[index : index + len(pattern)]) == pattern + for index in range(len(sequence) - len(pattern) + 1) + ) + + +def _same_foot_runs(chart: ChartDocument) -> list[int]: + # Pump panels 0/1 are left, 3/4 are right, and center alternates. This is + # an intentionally conservative proxy, not a claim about a player's feet. + runs: list[int] = [] + current_foot: str | None = None + current_length = 0 + center_next = "left" + for event in chart.events: + playable = [note for note in event.notes if note.type != "mine"] + if len(playable) != 1: + if current_length: + runs.append(current_length) + current_foot = None + current_length = 0 + continue + lane = playable[0].lane % 5 + foot = playable[0].foot + if foot is None: + if lane in {0, 1}: + foot = "left" + elif lane in {3, 4}: + foot = "right" + else: + foot = center_next + center_next = "right" if center_next == "left" else "left" + if foot == current_foot: + current_length += 1 + else: + if current_length: + runs.append(current_length) + current_foot = foot + current_length = 1 + if current_length: + runs.append(current_length) + return runs + + +def chart_statistics(chart: ChartDocument) -> ChartStatistics: + lane_counts = [0] * chart.lane_count + hold_count = jump_count = mine_count = note_count = 0 + note_times: list[float] = [] + measure_counts: Counter[int] = Counter() + for event in chart.events: + playable = 0 + for note in event.notes: + lane_counts[note.lane] += 1 + measure_counts[event.measure] += 1 + note_count += 1 + note_times.append(event.time_sec) + if note.type == "hold": + hold_count += 1 + playable += 1 + elif note.type == "mine": + mine_count += 1 + else: + playable += 1 + if playable >= 2: + jump_count += 1 + event_count = len(chart.events) + duration = max( + chart.duration_sec, + max(note_times, default=0.0) - min(0.0, min(note_times, default=0.0)), + ) + denominator = max(note_count, 1) + sequence = _primary_lane_sequence(chart) + foot_runs = _same_foot_runs(chart) + foot_steps = sum(foot_runs) + return ChartStatistics( + note_count=note_count, + event_count=event_count, + hold_count=hold_count, + jump_count=jump_count, + mine_count=mine_count, + duration_sec=max(0.0, duration), + nps_average=note_count / duration if duration > 0 else 0.0, + nps_peak=_peak_nps(note_times), + single_ratio=sum(len(event.notes) == 1 for event in chart.events) / max(event_count, 1), + jump_ratio=jump_count / max(event_count, 1), + hold_ratio=hold_count / denominator, + lane_counts=lane_counts, + measure_densities=[ + float(measure_counts.get(index, 0)) for index in range(chart.measure_count) + ], + same_foot_runs=foot_runs, + foot_switch_ratio=max(0, len(foot_runs) - 1) / max(foot_steps - 1, 1), + small_spin_count=_pattern_count(sequence, _SMALL_SPIN), + big_spin_count=_pattern_count(sequence, _BIG_SPIN), + ) + + +def corpus_statistics(charts: Iterable[ChartDocument]) -> CorpusStatistics: + values = list(charts) + single = [chart for chart in values if chart.mode == "pump-single"] + double = [chart for chart in values if chart.mode == "pump-double"] + transitions = [[1.0 for _ in range(5)] for _ in range(5)] + groups: Counter[str] = Counter() + songs: set[str] = set() + meter_values: defaultdict[int, list[ChartStatistics]] = defaultdict(list) + total_notes = 0 + total_duration = 0.0 + for chart in values: + stats = chart.statistics or chart_statistics(chart) + total_notes += stats.note_count + total_duration += stats.duration_sec + groups[chart.source_group or "UNKNOWN"] += 1 + songs.add(chart.music or chart.title) + meter_values[chart.meter].append(stats) + if chart.mode != "pump-single": + continue + for previous, current in _adjacent_single_lane_transitions(chart): + transitions[previous][current] += 1.0 + for row in transitions: + total = sum(row) + for index in range(5): + row[index] = row[index] / total + profiles: dict[str, dict[str, float]] = {} + for meter, items in sorted(meter_values.items()): + profiles[str(meter)] = { + "charts": float(len(items)), + "averageNps": sum(item.nps_average for item in items) / len(items), + "peakNps": sum(item.nps_peak for item in items) / len(items), + "jumpRatio": sum(item.jump_ratio for item in items) / len(items), + "holdRatio": sum(item.hold_ratio for item in items) / len(items), + } + meters = [chart.meter for chart in values] + return CorpusStatistics( + chart_count=len(values), + song_count=len(songs), + single_chart_count=len(single), + single_song_count=len({chart.music or chart.title for chart in single}), + double_chart_count=len(values) - len(single), + double_song_count=len({chart.music or chart.title for chart in double}), + difficulty_min=min(meters, default=0), + difficulty_max=max(meters, default=0), + total_notes=total_notes, + total_duration_sec=total_duration, + average_nps=total_notes / total_duration if total_duration > 0 else 0.0, + groups=dict(groups), + lane_transition_probabilities=transitions, + meter_profiles=profiles, + ) diff --git a/apps/api/beatforge_api/chart_engine/timing.py b/apps/api/beatforge_api/chart_engine/timing.py new file mode 100644 index 0000000..f2d8890 --- /dev/null +++ b/apps/api/beatforge_api/chart_engine/timing.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +from bisect import bisect_right + +from .models import TempoPoint + + +class TempoTimeline: + """Piecewise-constant StepMania tempo map with exact beat/time conversion.""" + + def __init__(self, changes: list[tuple[float, float]], offset_sec: float = 0.0): + normalized: dict[float, float] = {} + for beat, bpm in changes: + if beat < 0 or bpm <= 0: + continue + normalized[float(beat)] = float(bpm) + if not normalized: + raise ValueError("tempo map is empty") + ordered = sorted(normalized.items()) + if ordered[0][0] != 0.0: + ordered.insert(0, (0.0, ordered[0][1])) + self.offset_sec = float(offset_sec) + self.beats = [item[0] for item in ordered] + self.bpms = [item[1] for item in ordered] + self.times = [-self.offset_sec] + for index in range(1, len(ordered)): + beat_delta = self.beats[index] - self.beats[index - 1] + self.times.append(self.times[-1] + beat_delta * 60.0 / self.bpms[index - 1]) + + @property + def primary_bpm(self) -> float: + return self.bpms[0] + + def beat_to_time(self, beat: float) -> float: + index = max(0, bisect_right(self.beats, float(beat)) - 1) + return self.times[index] + (float(beat) - self.beats[index]) * 60.0 / self.bpms[index] + + def time_to_beat(self, time_sec: float) -> float: + index = max(0, bisect_right(self.times, float(time_sec)) - 1) + return self.beats[index] + (float(time_sec) - self.times[index]) * self.bpms[index] / 60.0 + + def bpm_at_beat(self, beat: float) -> float: + return self.bpms[max(0, bisect_right(self.beats, float(beat)) - 1)] + + def points(self) -> list[TempoPoint]: + return [ + TempoPoint(beat=beat, bpm=bpm, time_sec=time_sec) + for beat, bpm, time_sec in zip(self.beats, self.bpms, self.times, strict=True) + ] diff --git a/apps/api/beatforge_api/chart_engine/validator.py b/apps/api/beatforge_api/chart_engine/validator.py new file mode 100644 index 0000000..8da90fb --- /dev/null +++ b/apps/api/beatforge_api/chart_engine/validator.py @@ -0,0 +1,239 @@ +from __future__ import annotations + +import math +from collections import defaultdict, deque + +from .footwork import analyze_no_spin_footwork +from .models import ChartDocument, ValidationIssue, ValidationResult +from .rhythm_policy import density_limit_nps, maximum_subdivision +from .statistics import chart_statistics + +_COORDS = ((-1.0, -1.0), (-1.0, 1.0), (0.0, 0.0), (1.0, 1.0), (1.0, -1.0)) + + +def _distance(first: int, second: int) -> float: + a = _COORDS[first % 5] + b = _COORDS[second % 5] + return math.hypot(a[0] - b[0], a[1] - b[1]) + + +def _foot_for_lane(lane: int, center_foot: str) -> str: + panel = lane % 5 + if panel in {0, 1}: + return "left" + if panel in {3, 4}: + return "right" + return center_foot + + +def validate_chart(chart: ChartDocument) -> ValidationResult: + """Evaluate density, travel, hold conflicts, and rhythm-aware same-foot runs.""" + + issues: list[ValidationIssue] = [] + events = sorted(chart.events, key=lambda item: (item.time_sec, item.beat)) + difficulty = min(max(chart.meter, 1), 15) + density_limit = density_limit_nps(difficulty, bpm=chart.bpm) + subdivision_limit = maximum_subdivision(difficulty) + # Lv.11+ accepts the union of 1/16 (quarters of a beat) and 1/24 + # (sixths of a beat), whose common lattice is twelfths of a beat. + lattice_rows_per_beat = 4.0 if subdivision_limit == 16 else 12.0 + too_fine = [ + event + for event in events + if not math.isclose( + event.beat * lattice_rows_per_beat, + round(event.beat * lattice_rows_per_beat), + rel_tol=0.0, + abs_tol=1e-6, + ) + ] + if too_fine: + first = too_fine[0] + issues.append( + ValidationIssue( + code="SUBDIVISION_TOO_FINE_FOR_LEVEL", + severity="error", + message=( + f"Level {difficulty} supports at most 1/{subdivision_limit} rhythm; " + f"found 1/{first.subdivision}." + ), + time_sec=first.time_sec, + beat=first.beat, + penalty=20, + ) + ) + recent: deque[float] = deque() + peak_two_second_nps = 0.0 + for event in events: + playable = [note for note in event.notes if note.type != "mine"] + if len(playable) > 2: + issues.append( + ValidationIssue( + code="TOO_MANY_SIMULTANEOUS_PANELS", + severity="error", + message="An event requires more than two simultaneous panels.", + time_sec=event.time_sec, + beat=event.beat, + penalty=20, + ) + ) + for _note in playable: + recent.append(event.time_sec) + while recent and event.time_sec - recent[0] >= 2.0: + recent.popleft() + peak_two_second_nps = max(peak_two_second_nps, len(recent) / 2.0) + if peak_two_second_nps > density_limit: + issues.append( + ValidationIssue( + code="EXTREME_NPS", + severity="error" if peak_two_second_nps > density_limit * 1.25 else "warning", + message=( + f"Two-second density reaches {peak_two_second_nps:.2f} NPS; " + f"the level-{difficulty} limit is {density_limit:.2f}." + ), + penalty=min(25.0, (peak_two_second_nps - density_limit) * 4.0), + ) + ) + + holds_by_lane: defaultdict[int, list[tuple[float, float]]] = defaultdict(list) + for event in events: + for note in event.notes: + if note.type != "hold" or note.end_time_sec is None: + continue + for start, end in holds_by_lane[note.lane]: + if event.time_sec < end and note.end_time_sec > start: + issues.append( + ValidationIssue( + code="OVERLAPPING_HOLD", + severity="error", + message=f"Lane {note.lane + 1} contains overlapping holds.", + time_sec=event.time_sec, + beat=event.beat, + penalty=18, + ) + ) + break + holds_by_lane[note.lane].append((event.time_sec, note.end_time_sec)) + + disjoint_jump_transitions = 0 + if difficulty <= 10: + for previous, event in zip(events, events[1:], strict=False): + previous_playable = [note for note in previous.notes if note.type != "mine"] + playable = [note for note in event.notes if note.type != "mine"] + if ( + len(previous_playable) > 1 + and len(playable) > 1 + and event.beat - previous.beat <= 1.0 + 1e-9 + and {note.lane for note in previous_playable}.isdisjoint( + note.lane for note in playable + ) + ): + disjoint_jump_transitions += 1 + issues.append( + ValidationIssue( + code="DISJOINT_JUMP_TRANSITION", + severity="warning", + message=( + "Nearby jumps require both feet to reposition at once; " + "keep one row as a single panel." + ), + time_sec=event.time_sec, + beat=event.beat, + penalty=5, + ) + ) + + single_events = [ + (event, [note for note in event.notes if note.type != "mine"][0]) + for event in events + if len([note for note in event.notes if note.type != "mine"]) == 1 + ] + for (previous_event, previous), (event, note) in zip( + single_events, single_events[1:], strict=False + ): + interval = event.time_sec - previous_event.time_sec + travel = _distance(previous.lane, note.lane) + if interval < 0.095 and travel > 2.1: + issues.append( + ValidationIssue( + code="IMPOSSIBLE_TRAVEL", + severity="error", + message="A corner-to-corner move is too fast for reliable play.", + time_sec=event.time_sec, + beat=event.beat, + penalty=14, + ) + ) + + current_foot: str | None = None + run = 0 + center_foot = "left" + same_foot_violations = 0 + for event, note in single_events: + foot = note.foot or _foot_for_lane(note.lane, center_foot) + if note.foot is None and note.lane % 5 == 2: + center_foot = "right" if center_foot == "left" else "left" + run = run + 1 if foot == current_foot else 1 + current_foot = foot + if run == 10 and event.subdivision >= 16: + same_foot_violations += 1 + issues.append( + ValidationIssue( + code="SUSTAINED_SAME_FOOT_16TH", + severity="warning", + message="Ten or more 16th-or-faster notes are assigned to the same foot.", + time_sec=event.time_sec, + beat=event.beat, + penalty=8, + ) + ) + + footwork = analyze_no_spin_footwork(events) + enforce_full_step = not chart.spin_enabled and ( + chart.source_group == "BEATFORGE_GENERATED" + or chart.generator in {"local_chart_transformer", "real_corpus_profile_rules"} + ) + if enforce_full_step: + for violation in footwork.violations: + issues.append( + ValidationIssue( + code="NO_FULL_ALTERNATING_PATH", + severity="error", + message=( + "Strict left-right alternation reaches a back-facing stance; " + "reassign a panel or enable an explicit spin pattern." + ), + time_sec=violation.time_sec, + beat=violation.beat, + penalty=20, + ) + ) + + statistics = chart.statistics or chart_statistics(chart) + penalty = sum(issue.penalty for issue in issues) + score = max(0.0, min(100.0, 100.0 - penalty)) + return ValidationResult( + valid=not any(issue.severity == "error" for issue in issues), + score=score, + issues=issues, + metrics={ + "difficulty": difficulty, + "averageNps": statistics.nps_average, + "peakNps": statistics.nps_peak, + "peakTwoSecondNps": peak_two_second_nps, + "densityLimit": density_limit, + "densityNoteLimit": int(round(density_limit * 2.0)), + "maximumSubdivision": subdivision_limit, + "sameFootViolations": same_foot_violations, + "holdCount": statistics.hold_count, + "jumpCount": statistics.jump_count, + "disjointJumpTransitions": disjoint_jump_transitions, + "fullStepReachable": footwork.full_step_reachable, + "unplayableAlternatingSegments": len(footwork.violations), + "fullStepCheckedEvents": footwork.checked_events, + "fullStepSegments": footwork.segment_count, + "maxFacingAngle": footwork.max_abs_heading, + "crossoverCount": footwork.crossover_count, + "holdForcedRepeats": footwork.hold_forced_repeats, + }, + ) diff --git a/apps/api/beatforge_api/chart_routes.py b/apps/api/beatforge_api/chart_routes.py new file mode 100644 index 0000000..1148e80 --- /dev/null +++ b/apps/api/beatforge_api/chart_routes.py @@ -0,0 +1,402 @@ +from __future__ import annotations + +import hashlib +import json +import mimetypes +import re +from functools import lru_cache +from pathlib import Path +from typing import Annotated, Any +from urllib.parse import quote + +from fastapi import APIRouter, Depends, Header, Query +from fastapi.responses import Response, StreamingResponse +from sqlalchemy import select +from sqlalchemy.orm import Session, selectinload + +from .chart_engine.generator import generate_chart +from .chart_engine.library import ReferenceLibrary +from .chart_engine.models import ( + ChartDocument, + ChartGenerationResponse, + CorpusStatistics, + GenerateChartRequest, +) +from .chart_engine.sm import export_sm +from .config import get_settings +from .database import get_db +from .errors import BeatForgeError, not_found +from .models import TrackModel +from .serialization import candidate_event_dict + +chart_router = APIRouter(prefix="/api", tags=["chart-engine"]) +_GENERATION_ID = re.compile(r"^[a-f0-9]{20}$") + + +@lru_cache(maxsize=4) +def _library_for_root(root: str) -> ReferenceLibrary: + return ReferenceLibrary(root) + + +def _library() -> ReferenceLibrary: + return _library_for_root(str(get_settings().speed_charts_dir)) + + +@lru_cache(maxsize=4) +def _statistics_for_root(root: str) -> CorpusStatistics: + return _library_for_root(root).statistics() + + +def _corpus_statistics() -> CorpusStatistics: + return _statistics_for_root(str(get_settings().speed_charts_dir)) + + +@lru_cache(maxsize=2) +def _load_local_chart_model(path: str, modified_ns: int): + del modified_ns + from .chart_engine.learning import LocalChartModel + + return LocalChartModel.load(path, device="auto") + + +def _checkpoint_sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def _model_predictions( + track: TrackModel, difficulty: int, enabled: bool +) -> tuple[dict[str, dict[str, Any]] | None, dict[str, Any] | None, bool]: + checkpoint = get_settings().chart_models_dir / "chart-transformer.pt" + available = checkpoint.is_file() + if not enabled or not available or not track.candidate_events: + return None, None, available + analysis = { + "original_sample_rate": track.original_sample_rate, + "sample_count": track.sample_count, + "duration_sec": track.duration_sec, + "bpm": track.tempo_segments[0].bpm, + "bpm_confidence": track.tempo_segments[0].confidence, + "beat_offset_sample": track.tempo_segments[0].beat_offset_sample, + "candidate_events": [ + candidate_event_dict(candidate, track.original_sample_rate) + for candidate in track.candidate_events + ], + } + try: + checkpoint_stat = checkpoint.stat() + checkpoint_sha256 = _checkpoint_sha256(checkpoint) + runtime = _load_local_chart_model(str(checkpoint.resolve()), checkpoint_stat.st_mtime_ns) + inference = runtime.predict({"analysis": analysis}, difficulty=difficulty) + except Exception as exc: + raise BeatForgeError( + "LOCAL_CHART_MODEL_FAILED", + f"The local chart model could not run: {exc}", + status_code=409, + ) from exc + predictions = { + item.candidate_id: { + "laneProbabilities": list(item.lane_probabilities), + "holdProbability": item.hold_probability, + } + for item in inference.predictions + } + metadata = inference.checkpoint_metadata + provenance = { + "schemaVersion": metadata.get("schemaVersion"), + "architecture": metadata.get("architecture"), + "createdAt": metadata.get("createdAt"), + "datasetFingerprint": metadata.get("datasetFingerprint"), + "sampleCount": metadata.get("sampleCount"), + "bestLoss": metadata.get("bestLoss"), + "realDataOnly": metadata.get("realDataOnly") is True, + "checkpointSha256": checkpoint_sha256, + } + return predictions, provenance, available + + +def _get_track(session: Session, track_id: str) -> TrackModel: + statement = ( + select(TrackModel) + .where(TrackModel.id == track_id) + .options( + selectinload(TrackModel.project), + selectinload(TrackModel.hit_points), + selectinload(TrackModel.candidate_events), + selectinload(TrackModel.tempo_segments), + ) + ) + track = session.scalar(statement) + if not track: + raise not_found("track", track_id) + return track + + +def _parse_range(value: str, size: int) -> tuple[int, int]: + if not value.startswith("bytes=") or "," in value: + raise ValueError + start_text, end_text = value[6:].split("-", 1) + if not start_text: + length = int(end_text) + if length <= 0: + raise ValueError + return max(0, size - length), size - 1 + start = int(start_text) + end = int(end_text) if end_text else size - 1 + if start < 0 or start >= size or end < start: + raise ValueError + return start, min(end, size - 1) + + +def _audio_response(path: Path, range_header: str | None) -> StreamingResponse: + size = path.stat().st_size + start, end, status_code = 0, size - 1, 200 + if range_header: + try: + start, end = _parse_range(range_header, size) + except (TypeError, ValueError): + raise BeatForgeError( + "INVALID_RANGE", + "The requested byte range is invalid", + status_code=416, + details={"size": size}, + ) from None + status_code = 206 + + def chunks(): + remaining = end - start + 1 + with path.open("rb") as handle: + handle.seek(start) + while remaining > 0: + data = handle.read(min(1024 * 1024, remaining)) + if not data: + return + remaining -= len(data) + yield data + + headers = { + "Accept-Ranges": "bytes", + "Content-Length": str(end - start + 1), + "Content-Disposition": f"inline; filename*=UTF-8''{quote(path.name)}", + } + if status_code == 206: + headers["Content-Range"] = f"bytes {start}-{end}/{size}" + media_type = mimetypes.guess_type(path.name)[0] or "audio/mpeg" + return StreamingResponse( + chunks(), status_code=status_code, media_type=media_type, headers=headers + ) + + +def _generated_path(track_id: str, generation_id: str) -> Path: + if not _GENERATION_ID.fullmatch(generation_id): + raise BeatForgeError( + "INVALID_GENERATION_ID", "The generation id is invalid", status_code=404 + ) + root = get_settings().generated_charts_dir.resolve() + path = (root / track_id / f"{generation_id}.json").resolve() + if not path.is_relative_to(root): + raise BeatForgeError("INVALID_CHART_PATH", "The chart path is invalid", status_code=404) + return path + + +def _save_generated(track_id: str, chart: ChartDocument) -> None: + directory = get_settings().generated_charts_dir / track_id + directory.mkdir(parents=True, exist_ok=True) + payload = json.dumps( + chart.model_dump(by_alias=True, mode="json"), ensure_ascii=False, separators=(",", ":") + ) + path = directory / f"{chart.id}.json" + temporary = directory / f".{chart.id}.tmp" + temporary.write_text(payload, encoding="utf-8") + temporary.replace(path) + latest = directory / "latest.json" + latest_temporary = directory / ".latest.tmp" + latest_temporary.write_text(payload, encoding="utf-8") + latest_temporary.replace(latest) + + +def _load_generated(track_id: str, generation_id: str | None = None) -> ChartDocument: + if generation_id: + path = _generated_path(track_id, generation_id) + else: + root = get_settings().generated_charts_dir.resolve() + path = (root / track_id / "latest.json").resolve() + if not path.is_relative_to(root): + raise BeatForgeError("INVALID_CHART_PATH", "The chart path is invalid", status_code=404) + if not path.is_file(): + raise BeatForgeError( + "CHART_NOT_GENERATED", + "No generated chart is available for this track.", + status_code=404, + ) + try: + return ChartDocument.model_validate_json(path.read_text(encoding="utf-8")) + except (ValueError, OSError) as exc: + raise BeatForgeError( + "GENERATED_CHART_INVALID", + "The saved chart artifact is invalid.", + status_code=500, + ) from exc + + +@chart_router.get("/chart-engine/reference-charts") +def list_reference_charts( + mode: str | None = Query(default="pump-single", pattern=r"^(pump-single|pump-double)$"), + group: str | None = Query(default=None, pattern=r"^SPEED_(CLUB|DEVIL|REMIX)$"), + search: str = Query(default="", max_length=300), +) -> dict[str, Any]: + library = _library() + items = library.summaries(mode=mode, group=group, search=search) + return { + "items": [item.model_dump(by_alias=True, mode="json") for item in items], + "total": len(items), + "corpusTotal": len(library), + "source": "local_reference_corpus", + } + + +@chart_router.get("/chart-engine/reference-charts/{chart_id}", response_model=ChartDocument) +def get_reference_chart(chart_id: str) -> ChartDocument: + try: + return _library().chart(chart_id) + except KeyError: + raise BeatForgeError( + "REFERENCE_CHART_NOT_FOUND", "Reference chart not found", status_code=404 + ) from None + + +@chart_router.get("/chart-engine/reference-charts/{chart_id}/audio") +def get_reference_audio( + chart_id: str, + range_header: Annotated[str | None, Header(alias="Range")] = None, +) -> StreamingResponse: + try: + asset = _library().asset(chart_id) + except KeyError: + raise BeatForgeError( + "REFERENCE_CHART_NOT_FOUND", "Reference chart not found", status_code=404 + ) from None + return _audio_response(asset.audio_path, range_header) + + +@chart_router.head("/chart-engine/reference-charts/{chart_id}/audio") +def head_reference_audio(chart_id: str) -> Response: + try: + asset = _library().asset(chart_id) + except KeyError: + raise BeatForgeError( + "REFERENCE_CHART_NOT_FOUND", "Reference chart not found", status_code=404 + ) from None + media_type = mimetypes.guess_type(asset.audio_path.name)[0] or "audio/mpeg" + return Response( + media_type=media_type, + headers={ + "Accept-Ranges": "bytes", + "Content-Length": str(asset.audio_path.stat().st_size), + "Content-Disposition": f"inline; filename*=UTF-8''{quote(asset.audio_path.name)}", + }, + ) + + +@chart_router.get("/chart-engine/statistics", response_model=CorpusStatistics) +def get_corpus_statistics() -> CorpusStatistics: + if len(_library()) == 0: + raise BeatForgeError( + "REFERENCE_CORPUS_NOT_FOUND", + "The local reference chart corpus is not available.", + status_code=404, + ) + return _corpus_statistics() + + +@chart_router.post("/tracks/{track_id}/chart/generate", response_model=ChartGenerationResponse) +def generate_track_chart( + track_id: str, + payload: GenerateChartRequest, + session: Annotated[Session, Depends(get_db)], +) -> ChartGenerationResponse: + track = _get_track(session, track_id) + if not track.tempo_segments or not (track.candidate_events or track.hit_points): + raise BeatForgeError( + "CHART_ANALYSIS_REQUIRED", + "Run BeatForge analysis before generating a chart.", + status_code=409, + ) + if len(_library()) == 0: + raise BeatForgeError( + "REFERENCE_CORPUS_NOT_FOUND", + "The local reference chart corpus is not available.", + status_code=404, + ) + corpus = _corpus_statistics() + model_predictions, model_provenance, model_available = _model_predictions( + track, payload.difficulty, payload.use_local_model + ) + try: + chart = generate_chart( + track_id=track.id, + title=track.project.title, + artist=track.project.artist, + music=track.original_file_name, + duration_sec=track.duration_sec, + sample_rate=track.original_sample_rate, + tempo_segments=track.tempo_segments, + candidates=track.candidate_events, + hit_points=track.hit_points, + difficulty=payload.difficulty, + enable_spin=payload.enable_spin, + seed=payload.seed, + transition_probabilities=corpus.lane_transition_probabilities, + model_predictions=model_predictions, + model_provenance=model_provenance, + ) + except ValueError as exc: + raise BeatForgeError("CHART_GENERATION_FAILED", str(exc), status_code=422) from exc + _save_generated(track.id, chart) + return ChartGenerationResponse( + generation_id=chart.id, + chart=chart, + reference_corpus={ + "source": "SPEED_DEVIL + SPEED_REMIX + pump-single SPEED_CLUB", + "chartCount": corpus.single_chart_count, + "songCount": corpus.single_song_count, + "difficultyRange": [corpus.difficulty_min, min(corpus.difficulty_max, 15)], + "model": { + "requested": payload.use_local_model, + "available": model_available, + "used": model_predictions is not None, + }, + }, + ) + + +@chart_router.get("/tracks/{track_id}/chart/latest", response_model=ChartDocument) +def get_latest_track_chart( + track_id: str, session: Annotated[Session, Depends(get_db)] +) -> ChartDocument: + _get_track(session, track_id) + return _load_generated(track_id) + + +@chart_router.get("/tracks/{track_id}/chart/export") +def export_track_chart( + track_id: str, + session: Annotated[Session, Depends(get_db)], + generation_id: str | None = Query(default=None, alias="generationId"), +) -> Response: + track = _get_track(session, track_id) + chart = _load_generated(track_id, generation_id) + filename = f"{Path(track.original_file_name).stem}_Lv{chart.meter}.sm" + return Response( + export_sm(chart).encode("utf-8-sig"), + media_type="text/plain; charset=utf-8", + headers={ + "Content-Disposition": ( + f"attachment; filename=beatforge-Lv{chart.meter}.sm; " + f"filename*=UTF-8''{quote(filename)}" + ) + }, + ) diff --git a/apps/api/beatforge_api/config.py b/apps/api/beatforge_api/config.py index b030917..b21a717 100644 --- a/apps/api/beatforge_api/config.py +++ b/apps/api/beatforge_api/config.py @@ -43,6 +43,30 @@ def vocal_alignment_dir(self) -> Path: def alignment_dir(self) -> Path: return self.storage_dir / "alignment" + @property + def chart_engine_dir(self) -> Path: + return self.storage_dir / "chart-engine" + + @property + def generated_charts_dir(self) -> Path: + return self.chart_engine_dir / "generated" + + @property + def chart_dataset_dir(self) -> Path: + return self.chart_engine_dir / "dataset" + + @property + def chart_models_dir(self) -> Path: + return self.chart_engine_dir / "models" + + @property + def speed_charts_dir(self) -> Path: + configured = os.environ.get("BEATFORGE_SPEED_CHARTS_DIR", "").strip() + if configured: + path = Path(configured).expanduser() + return path.resolve() if path.is_absolute() else (self.project_root / path).resolve() + return (self.project_root / "local-data" / "speed-corpus").resolve() + def ensure_directories(self) -> None: for directory in ( self.storage_dir, @@ -53,6 +77,10 @@ def ensure_directories(self) -> None: self.models_dir, self.vocal_alignment_dir, self.alignment_dir, + self.chart_engine_dir, + self.generated_charts_dir, + self.chart_dataset_dir, + self.chart_models_dir, ): directory.mkdir(parents=True, exist_ok=True) diff --git a/apps/api/beatforge_api/main.py b/apps/api/beatforge_api/main.py index 3b5b41c..fbda53b 100644 --- a/apps/api/beatforge_api/main.py +++ b/apps/api/beatforge_api/main.py @@ -10,6 +10,7 @@ from sqlalchemy import select from starlette.exceptions import HTTPException +from .chart_routes import chart_router from .config import get_settings from .database import SessionLocal, init_db from .errors import BeatForgeError @@ -80,6 +81,7 @@ async def lifespan(_app: FastAPI): expose_headers=["Content-Range", "Accept-Ranges", "Content-Disposition"], ) app.include_router(router) +app.include_router(chart_router) @app.exception_handler(BeatForgeError) diff --git a/apps/api/beatforge_api/media.py b/apps/api/beatforge_api/media.py index 15f9a52..f2ddbaa 100644 --- a/apps/api/beatforge_api/media.py +++ b/apps/api/beatforge_api/media.py @@ -162,13 +162,21 @@ def probe_audio(path: Path) -> AudioProbe: ) -def prepare_analysis_source(path: Path, output_dir: Path) -> tuple[Path, bool]: - """Return a libsndfile-readable source, decoding through ffmpeg when necessary.""" - try: - sf.info(str(path)) - return path, False - except RuntimeError: - pass +def prepare_analysis_source( + path: Path, output_dir: Path, *, force_ffmpeg: bool = False +) -> tuple[Path, bool]: + """Return a libsndfile-readable source, decoding through ffmpeg when necessary. + + ``sf.info`` can succeed for a damaged or unusual MP3 even when decoding the + complete stream later fails. Callers that already observed a libsndfile + decode error can therefore force the existing ffmpeg recovery path. + """ + if not force_ffmpeg: + try: + sf.info(str(path)) + return path, False + except RuntimeError: + pass ffmpeg = shutil.which("ffmpeg") if not ffmpeg: raise BeatForgeError( diff --git a/apps/api/pyproject.toml b/apps/api/pyproject.toml index d46e62c..fb880da 100644 --- a/apps/api/pyproject.toml +++ b/apps/api/pyproject.toml @@ -37,6 +37,9 @@ accurate = [ "torch==2.13.0", "torchaudio==2.11.0", ] +chart-ml = [ + "torch==2.13.0", +] [tool.hatch.build.targets.wheel] packages = ["beatforge_api"] diff --git a/apps/api/tests/test_chart_learning.py b/apps/api/tests/test_chart_learning.py new file mode 100644 index 0000000..fd7afcd --- /dev/null +++ b/apps/api/tests/test_chart_learning.py @@ -0,0 +1,122 @@ +from __future__ import annotations + +import json +import os +from pathlib import Path + +import pytest + +pytest.importorskip("torch", reason="chart sequence-model tests require beatforge-api[chart-ml]") + +from beatforge_api.chart_engine.learning import ( # noqa: E402 + CHECKPOINT_SCHEMA_VERSION, + FEATURE_NAMES, + LocalChartModel, + TrainingConfig, + candidate_records, + load_completed_dataset_samples, + sequence_example, + train_chart_transformer, +) +from beatforge_api.chart_engine.model import ChartTransformerConfig # noqa: E402 + + +def _real_dataset_root() -> Path: + configured = os.environ.get("BEATFORGE_CHART_DATASET_DIR", "").strip() + root = ( + Path(configured).expanduser().resolve() + if configured + else Path(__file__).resolve().parents[3] / "storage" / "chart-engine" / "dataset" + ) + if not root.is_dir(): + pytest.skip("the completed real SPEED chart dataset is not available") + has_real_train = False + for metadata_path in root.glob("*/metadata.json"): + metadata = json.loads(metadata_path.read_text(encoding="utf-8")) + if metadata.get("realData") is True and metadata.get("split") == "train": + has_real_train = True + break + if not has_real_train: + pytest.skip("the completed real SPEED dataset has no training split") + return root + + +def test_real_dataset_candidate_features_and_targets_are_aligned() -> None: + sample = load_completed_dataset_samples( + _real_dataset_root(), split="train", verify_audio_hashes=False + )[0] + example = sequence_example(sample) + + assert sample.metadata["realData"] is True + assert sample.chart.mode == "pump-single" + assert len(example.records) == len(example.lane_targets) == len(example.hold_targets) + assert example.matched_event_count > 0 + assert len(example.records[0].features) == len(FEATURE_NAMES) + assert any(any(lanes) for lanes in example.lane_targets) + + +def test_real_dataset_manifest_closes_all_five_lane_training_triples() -> None: + root = _real_dataset_root() + manifest = json.loads((root / "manifest.json").read_text(encoding="utf-8")) + report = json.loads((root / "build_report.json").read_text(encoding="utf-8")) + samples = load_completed_dataset_samples(root, verify_audio_hashes=False) + + assert manifest["realDataOnly"] is True + assert manifest["mode"] == "pump-single" + assert manifest["sampleCount"] == len(samples) == 55 + assert manifest["uniqueAudioCount"] == len({sample.audio_sha256 for sample in samples}) == 52 + assert sum(manifest["splits"].values()) == 55 + assert report["sourceChartCount"] == report["completed"] == 55 + assert report["failed"] == report["skipped"] == 0 + assert all(sample.metadata["realData"] is True for sample in samples) + + +def test_train_checkpoint_and_local_inference_use_real_candidates(tmp_path: Path) -> None: + dataset_root = _real_dataset_root() + checkpoint = tmp_path / "chart-transformer.pt" + result = train_chart_transformer( + dataset_root, + checkpoint, + training=TrainingConfig( + epochs=1, + batch_size=1, + sequence_length=64, + validation_split=None, + verify_audio_hashes=False, + max_batches_per_epoch=1, + device="cpu", + seed=17, + ), + model_config=ChartTransformerConfig( + input_dim=len(FEATURE_NAMES), + d_model=16, + nhead=2, + num_layers=1, + dim_feedforward=32, + dropout=0.0, + max_sequence_length=64, + ), + ) + + assert result.checkpoint_path == checkpoint.resolve() + assert result.metadata["schemaVersion"] == CHECKPOINT_SCHEMA_VERSION + assert result.metadata["realDataOnly"] is True + assert result.metadata["trainSampleCount"] > 0 + assert result.metadata["matchedEventCount"] > 0 + + sample = load_completed_dataset_samples(dataset_root, split="train", verify_audio_hashes=False)[ + 0 + ] + expected = candidate_records(sample.beatforge) + runtime = LocalChartModel.load(checkpoint, device="cpu") + inference = runtime.predict(sample.beatforge, difficulty=sample.training_difficulty) + + assert len(inference.predictions) == len(expected) + assert inference.predictions[0].candidate_id == expected[0].candidate_id + assert len(inference.predictions[0].lane_probabilities) == 5 + assert all( + 0.0 <= probability <= 1.0 + for prediction in inference.predictions + for probability in prediction.lane_probabilities + ) + assert all(0.0 <= prediction.hold_probability <= 1.0 for prediction in inference.predictions) diff --git a/apps/api/tests/test_chart_library_privacy.py b/apps/api/tests/test_chart_library_privacy.py new file mode 100644 index 0000000..5f76536 --- /dev/null +++ b/apps/api/tests/test_chart_library_privacy.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +from pathlib import Path + +import pytest + +from beatforge_api.chart_engine import library as library_module +from beatforge_api.chart_engine.library import ReferenceLibrary +from beatforge_api.chart_engine.models import ChartDocument, TempoPoint + + +def test_reference_chart_never_exposes_the_corpus_root( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + corpus_root = tmp_path / "licensed-corpus" + song_dir = corpus_root / "SPEED_CLUB" / "synthetic-song" + song_dir.mkdir(parents=True) + chart_path = song_dir / "synthetic_Lv5.sm" + audio_path = song_dir / "synthetic.mp3" + chart_path.write_text("synthetic parser input", encoding="utf-8") + audio_path.write_bytes(b"synthetic audio placeholder") + + parsed = ChartDocument( + id="synthetic-chart", + title="Synthetic chart", + artist="BeatForge Test Lab", + music=audio_path.name, + source_group="SPEED_CLUB", + source_path=str(chart_path.resolve()), + meter=5, + bpm=120, + duration_sec=8, + tempo_map=[TempoPoint(beat=0, bpm=120, time_sec=0)], + events=[], + ) + monkeypatch.setattr(library_module, "_cached_chart", lambda *args: parsed) + + library = ReferenceLibrary(corpus_root) + result = library.chart(library.assets()[0].id) + + assert result.source_path == "SPEED_CLUB/synthetic-song/synthetic_Lv5.sm" + assert not Path(result.source_path).is_absolute() + assert str(tmp_path) not in result.model_dump_json() diff --git a/apps/api/tests/test_chart_model_integration.py b/apps/api/tests/test_chart_model_integration.py new file mode 100644 index 0000000..dff973a --- /dev/null +++ b/apps/api/tests/test_chart_model_integration.py @@ -0,0 +1,1351 @@ +from __future__ import annotations + +import hashlib +import json +import math +from dataclasses import replace +from pathlib import Path +from typing import Any + +import pytest +from fastapi.testclient import TestClient + +from beatforge_api import chart_routes +from beatforge_api.chart_engine import export_sm, parse_sm, validate_chart +from beatforge_api.chart_engine.footwork import analyze_no_spin_footwork +from beatforge_api.chart_engine.generator import ( + _source_points, + _tempo_timeline, + generate_chart, +) +from beatforge_api.chart_engine.learning import ( + FEATURE_NAMES, + RealDatasetSample, + TrainingConfig, + TrainingResult, + load_completed_dataset_samples, + train_chart_transformer, +) +from beatforge_api.chart_engine.model import ChartTransformerConfig +from beatforge_api.chart_engine.models import ChartDocument, ChartEvent, ChartNote +from beatforge_api.chart_engine.optimizer import optimize_events +from beatforge_api.chart_engine.statistics import corpus_statistics +from beatforge_api.config import Settings, get_settings +from beatforge_api.database import SessionLocal +from beatforge_api.models import ( + CandidateEventModel, + HitPointModel, + ProjectModel, + TempoSegmentModel, + TrackModel, +) +from beatforge_api.serialization import dumps + + +def _real_speed_dataset() -> Path: + root = get_settings().project_root / "storage" / "chart-engine" / "dataset" + if not root.is_dir(): + pytest.skip("the completed real SPEED chart dataset is not available") + return root + + +@pytest.fixture(scope="module") +def real_speed_sample() -> RealDatasetSample: + try: + samples = load_completed_dataset_samples( + _real_speed_dataset(), split="train", verify_audio_hashes=False + ) + except ValueError as exc: + pytest.skip(f"the completed real SPEED training split is unavailable: {exc}") + return min( + samples, + key=lambda sample: len(sample.beatforge["analysis"]["candidate_events"]), + ) + + +@pytest.fixture(scope="module") +def tiny_real_checkpoint(tmp_path_factory: pytest.TempPathFactory) -> TrainingResult: + pytest.importorskip( + "torch", reason="local-checkpoint route integration requires beatforge-api[chart-ml]" + ) + storage_dir = tmp_path_factory.mktemp("chart-route-model") / "storage" + checkpoint = storage_dir / "chart-engine" / "models" / "chart-transformer.pt" + result = train_chart_transformer( + _real_speed_dataset(), + checkpoint, + training=TrainingConfig( + epochs=1, + batch_size=1, + sequence_length=64, + validation_split=None, + verify_audio_hashes=False, + max_batches_per_epoch=1, + device="cpu", + seed=29, + ), + model_config=ChartTransformerConfig( + input_dim=len(FEATURE_NAMES), + d_model=16, + nhead=2, + num_layers=1, + dim_feedforward=32, + dropout=0.0, + max_sequence_length=64, + ), + ) + return result + + +def _use_route_storage(monkeypatch: pytest.MonkeyPatch, storage_dir: Path) -> Settings: + settings = replace(get_settings(), storage_dir=storage_dir.resolve()) + settings.ensure_directories() + monkeypatch.setattr(chart_routes, "get_settings", lambda: settings) + chart_routes._load_local_chart_model.cache_clear() + return settings + + +def _seed_real_speed_track(sample: RealDatasetSample) -> str: + analysis = sample.beatforge["analysis"] + track_id = "10000000-0000-0000-0000-000000000001" + project = ProjectModel( + id="20000000-0000-0000-0000-000000000002", + title=sample.chart.title, + artist=sample.chart.artist, + genre="SPEED", + status="completed", + ) + track = TrackModel( + id=track_id, + project=project, + original_file_name=sample.chart.music, + stored_file_name=f"{sample.sample_id}.mp3", + file_path=str(sample.sample_dir / "audio.mp3"), + format="mp3", + original_sample_rate=int(analysis["original_sample_rate"]), + channels=int(analysis.get("channels", 2)), + sample_count=int(analysis["sample_count"]), + duration_sec=float(analysis["duration_sec"]), + leading_silence_samples=int(analysis.get("leading_silence_samples", 0)), + analysis_json="{}", + ) + track.tempo_segments.append( + TempoSegmentModel( + start_sample=0, + bpm=float(analysis["bpm"]), + beat_offset_sample=int(analysis.get("beat_offset_sample", 0)), + confidence=float(analysis.get("bpm_confidence", 0.0)), + ) + ) + for item in analysis.get("hit_points", []): + track.hit_points.append(_hit_point_model(item)) + with SessionLocal() as session: + session.add(project) + session.flush() + for item in analysis["candidate_events"]: + track.candidate_events.append(_candidate_model(item)) + session.commit() + return track_id + + +def _candidate_model(item: dict[str, Any]) -> CandidateEventModel: + acoustic_sample = int(item["acoustic_sample"]) + return CandidateEventModel( + id=str(item["id"]), + sample=acoustic_sample, + acoustic_sample=acoustic_sample, + chart_sample=int(item.get("chart_sample", acoustic_sample)), + hit_point_id=item.get("hit_point_id"), + snap_error_ms=float(item.get("snap_error_ms", 0.0)), + lane=str(item.get("lane", "mix")), + source_evidence_json=dumps(item.get("source_evidence", {})), + semantic_evidence_json=dumps(item.get("semantic_evidence", {})), + confidence=float(item.get("confidence", 0.0)), + status=str(item.get("status", "uncertain")), + grid_type=str(item.get("grid_type", "straight_1_16")), + grid_confidence=float(item.get("grid_confidence", 0.0)), + source=str(item.get("source", item.get("lane", "mix"))), + generator=str(item.get("generator", "analysis")), + character=item.get("character"), + mora=item.get("mora"), + phoneme=item.get("phoneme"), + event_level=str(item.get("event_level", "analysis")), + event_policy=item.get("event_policy"), + alignment_unit_id=item.get("alignment_unit_id"), + alignment_unit_index=item.get("alignment_unit_index"), + alignment_run_id=item.get("alignment_run_id"), + character_indices_json=json.dumps(item.get("character_indices", [])), + phonemes_json=json.dumps(item.get("phonemes", [])), + aligned_sample=item.get("aligned_sample"), + refined_sample=item.get("refined_sample"), + evidence_json=dumps(item.get("evidence", {})), + ) + + +def _hit_point_model(item: dict[str, Any]) -> HitPointModel: + sample = int(item.get("sample", item["acoustic_sample"])) + acoustic_sample = int(item.get("acoustic_sample", sample)) + chart_sample = int(item.get("chart_sample", acoustic_sample)) + return HitPointModel( + id=str(item["id"]), + sample=sample, + acoustic_sample=acoustic_sample, + chart_sample=chart_sample, + detected_sample=int(item.get("detected_sample", acoustic_sample)), + refined_sample=int(item.get("refined_sample", acoustic_sample)), + snapped_sample=int(item.get("snapped_sample", chart_sample)), + snap_error_ms=float(item.get("snap_error_ms", 0.0)), + band=str(item.get("band", "full_band_accent")), + confidence=float(item.get("confidence", 0.0)), + salience=float(item.get("salience", item.get("confidence", 0.0))), + source=str(item.get("source", "fused")), + detector_votes_json=dumps(item.get("detector_votes", [])), + primary_stem=str(item.get("primary_stem", "mix")), + stem_evidence_json=dumps(item.get("stem_evidence", {})), + manually_edited=bool(item.get("manually_edited", False)), + locked=bool(item.get("locked", False)), + ) + + +def _generate(client: TestClient, track_id: str, *, use_model: bool) -> dict[str, Any]: + response = client.post( + f"/api/tracks/{track_id}/chart/generate", + json={ + "difficulty": 7, + "enableSpin": False, + "useLocalModel": use_model, + "seed": 20_260_721, + }, + ) + assert response.status_code == 200, response.text + return response.json() + + +def test_model_suppresses_low_probability_real_candidate_events( + real_speed_sample: RealDatasetSample, +) -> None: + analysis = real_speed_sample.beatforge["analysis"] + # Uncertain proposals remain model-selectable; this deliberately does not + # exercise the accepted BeatForge evidence policy covered by the next tests. + candidates = [ + {**candidate, "status": "uncertain"} for candidate in analysis["candidate_events"] + ] + selected = candidates[len(candidates) // 2] + predictions = { + str(item["id"]): { + "laneProbabilities": [0.01, 0.01, 0.01, 0.01, 0.01], + "holdProbability": 0.01, + } + for item in candidates + } + predictions[str(selected["id"])] = { + "laneProbabilities": [0.99, 0.01, 0.01, 0.01, 0.01], + "holdProbability": 0.01, + } + + chart = generate_chart( + track_id=real_speed_sample.sample_id, + title=real_speed_sample.chart.title, + artist=real_speed_sample.chart.artist, + music=real_speed_sample.chart.music, + duration_sec=float(analysis["duration_sec"]), + sample_rate=int(analysis["original_sample_rate"]), + tempo_segments=[ + { + "start_sample": 0, + "bpm": analysis["bpm"], + "beat_offset_sample": analysis.get("beat_offset_sample", 0), + } + ], + candidates=candidates, + # Candidate events are model-selectable. Confirmed hit points have a + # separate hard-anchor contract covered below. + hit_points=[], + difficulty=real_speed_sample.training_difficulty, + seed=20_260_721, + model_predictions=predictions, + model_provenance={"checkpointSha256": "a" * 64}, + ) + + assert len(chart.events) == 1 + assert chart.events[0].source_event_id == selected["id"] + assert chart.generator == "local_chart_transformer" + + +def test_opening_accepted_hubert_mora_sequence_has_no_silent_hole() -> None: + """Three accepted 1/16 markers must remain three event rows.""" + + sample_rate = 48_000 + candidates = [ + { + "id": "opening-marker-1", + "sample": 78_000, + "acoustic_sample": 78_000, + "chart_sample": 78_000, + "hit_point_id": "opening-hit-1", + "confidence": 0.9, + "grid_confidence": 0.95, + "status": "accepted", + "source": "mix", + "generator": "analysis", + "event_level": "analysis", + }, + { + "id": "opening-marker-2", + "sample": 84_000, + "acoustic_sample": 84_000, + "chart_sample": 84_000, + "hit_point_id": None, + "confidence": 0.85, + "grid_confidence": 0.9, + "status": "accepted", + "source": "vocals", + "generator": "hubert_ctc", + "character": "A", + "mora": "a", + "phoneme": "a", + "event_level": "mora", + }, + { + "id": "opening-marker-3", + "sample": 90_000, + "acoustic_sample": 90_000, + "chart_sample": 90_000, + "hit_point_id": None, + "confidence": 0.8, + "grid_confidence": 0.85, + "status": "accepted", + "source": "vocals", + "generator": "hubert_ctc", + "character": "B", + "mora": "b", + "phoneme": "b", + "event_level": "mora", + }, + ] + predictions = { + candidates[0]["id"]: { + "laneProbabilities": [ + 0.1, + 0.8, + 0.2, + 0.4, + 0.1, + ], + "holdProbability": 0.01, + }, + candidates[1]["id"]: { + "laneProbabilities": [ + 0.15, + 0.75, + 0.1, + 0.3, + 0.05, + ], + "holdProbability": 0.01, + }, + candidates[2]["id"]: { + "laneProbabilities": [ + 0.2, + 0.6, + 0.15, + 0.35, + 0.1, + ], + "holdProbability": 0.01, + }, + } + + chart = generate_chart( + track_id="opening-sequence-regression", + title="Opening marker contract", + artist="BeatForge test fixture", + music="opening-sequence.mp3", + duration_sec=8.0, + sample_rate=sample_rate, + tempo_segments=[ + { + "start_sample": 0, + "bpm": 120.0, + "beat_offset_sample": 0, + } + ], + candidates=candidates, + hit_points=[], + difficulty=9, + seed=42, + model_predictions=predictions, + model_provenance={"checkpointSha256": "a" * 64}, + ) + + opening_events = [event for event in chart.events if event.beat in {3.25, 3.5, 3.75}] + assert {event.beat for event in opening_events} == {3.25, 3.5, 3.75} + assert all(len(event.notes) == 1 for event in opening_events) + assert {candidate["id"] for candidate in candidates} <= { + source_id for event in opening_events for source_id in event.source_event_ids + } + + +@pytest.mark.parametrize( + ("seed", "lane_probabilities"), + [ + (15, None), + (0, [0.99, 0.01, 0.01, 0.01, 0.01]), + ], +) +def test_lv8_scattered_marker_does_not_gain_an_unjustified_jump( + seed: int, + lane_probabilities: list[float] | None, +) -> None: + sample_rate = 48_000 + beat = 4.0 + sample = int(beat * 0.5 * sample_rate) + candidate_id = f"scattered-marker-{seed}" + predictions = {} + if lane_probabilities is not None: + predictions[candidate_id] = { + "laneProbabilities": lane_probabilities, + "holdProbability": 0.0, + } + chart = generate_chart( + track_id=candidate_id, + title="Scattered marker contract", + artist="BeatForge", + music="single.mp3", + duration_sec=8.0, + sample_rate=sample_rate, + tempo_segments=[{"start_sample": 0, "bpm": 120.0, "beat_offset_sample": 0}], + candidates=[ + { + "id": candidate_id, + "sample": sample, + "acoustic_sample": sample, + "chart_sample": sample, + "confidence": 0.9, + "grid_confidence": 0.9, + "status": "accepted", + "source": "mix", + "generator": "analysis", + "event_level": "analysis", + } + ], + hit_points=[], + difficulty=8, + seed=seed, + model_predictions=predictions, + ) + + assert len(chart.events) == 1 + assert len(chart.events[0].notes) == 1 + + +def test_sparse_model_marker_can_still_generate_a_jump() -> None: + sample_rate = 48_000 + beat = 4.0 + sample = int(beat * 0.5 * sample_rate) + candidate_id = "sparse-model-jump" + chart = generate_chart( + track_id="sparse-model-jump", + title="Sparse jump contract", + artist="BeatForge", + music="jump.mp3", + duration_sec=8.0, + sample_rate=sample_rate, + tempo_segments=[{"start_sample": 0, "bpm": 120.0, "beat_offset_sample": 0}], + candidates=[ + { + "id": candidate_id, + "sample": sample, + "acoustic_sample": sample, + "chart_sample": sample, + "confidence": 0.9, + "grid_confidence": 0.9, + "status": "uncertain", + "source": "mix", + "generator": "analysis", + "event_level": "analysis", + } + ], + hit_points=[], + difficulty=8, + seed=20_260_721, + model_predictions={ + candidate_id: { + "laneProbabilities": [0.99, 0.90, 0.01, 0.01, 0.01], + "holdProbability": 0.0, + } + }, + model_provenance={"checkpointSha256": "a" * 64}, + ) + + assert len(chart.events) == 1 + assert len(chart.events[0].notes) == 2 + assert {note.lane for note in chart.events[0].notes} == {0, 1} + + +def test_lv8_full_band_downbeat_can_generate_an_accent_jump() -> None: + sample_rate = 48_000 + beat = 4.0 + sample = int(beat * 0.5 * sample_rate) + chart = generate_chart( + track_id="full-band-accent-jump", + title="Full-band accent contract", + artist="BeatForge", + music="accent.mp3", + duration_sec=8.0, + sample_rate=sample_rate, + tempo_segments=[{"start_sample": 0, "bpm": 120.0, "beat_offset_sample": 0}], + candidates=[], + hit_points=[ + { + "id": "full-band-accent-hit", + "sample": sample, + "chart_sample": sample, + "confidence": 0.9, + "grid_confidence": 0.9, + "salience": 0.9, + "band": "full_band_accent", + "primary_stem": "drums", + } + ], + difficulty=8, + seed=20_260_721, + model_predictions={}, + ) + + assert len(chart.events) == 1 + assert len(chart.events[0].notes) == 2 + + +def test_lv10_quantizes_accepted_marker_to_sixteenth_grid() -> None: + sample_rate = 48_000 + source_beat = 4.0 + 1.0 / 6.0 + source_sample = int(round(source_beat * 0.5 * sample_rate)) + chart = generate_chart( + track_id="lv10-sixteenth-grid", + title="Lv10 grid contract", + artist="BeatForge", + music="grid.mp3", + duration_sec=8.0, + sample_rate=sample_rate, + tempo_segments=[{"start_sample": 0, "bpm": 120.0, "beat_offset_sample": 0}], + candidates=[ + { + "id": "accepted-triplet-shaped-marker", + "sample": source_sample, + "acoustic_sample": source_sample, + "chart_sample": source_sample, + "confidence": 0.9, + "grid_confidence": 0.1, + "grid_type": "straight_1_16", + "status": "accepted", + "source": "vocals", + "generator": "hubert_ctc", + "event_level": "mora", + } + ], + hit_points=[], + difficulty=10, + seed=20_260_721, + ) + + assert len(chart.events) == 1 + assert chart.events[0].subdivision == 16 + assert math.isclose(chart.events[0].beat, 4.25, rel_tol=0.0, abs_tol=1e-9) + assert chart.validation is not None and chart.validation.valid + + +def test_lv11_preserves_mixed_sixteenth_and_twenty_fourth_markers( + tmp_path: Path, +) -> None: + sample_rate = 48_000 + + def accepted_marker(marker_id: str, beat: float, *, grid_confidence: float) -> dict[str, Any]: + sample = int(round(beat * 0.5 * sample_rate)) + return { + "id": marker_id, + "sample": sample, + "acoustic_sample": sample, + "chart_sample": sample, + "confidence": 0.9, + "grid_confidence": grid_confidence, + "grid_type": "straight_1_16", + "status": "accepted", + "source": "vocals", + "generator": "hubert_ctc", + "event_level": "mora", + } + + sixteenth_beat = 4.25 + twenty_fourth_beat = 4.0 + 1.0 / 6.0 + chart = generate_chart( + track_id="lv11-mixed-grid", + title="Lv11 grid contract", + artist="BeatForge", + music="grid.mp3", + duration_sec=8.0, + sample_rate=sample_rate, + tempo_segments=[{"start_sample": 0, "bpm": 120.0, "beat_offset_sample": 0}], + candidates=[ + accepted_marker("sixteenth-marker", sixteenth_beat, grid_confidence=0.95), + accepted_marker("twenty-fourth-marker", twenty_fourth_beat, grid_confidence=0.10), + ], + hit_points=[], + difficulty=11, + seed=20_260_721, + ) + + assert any( + event.subdivision == 16 + and math.isclose(event.beat, sixteenth_beat, rel_tol=0.0, abs_tol=1e-9) + for event in chart.events + ) + assert any( + event.subdivision == 24 + and math.isclose(event.beat, twenty_fourth_beat, rel_tol=0.0, abs_tol=1e-9) + for event in chart.events + ) + assert chart.validation is not None and chart.validation.valid + + lv10_validation = validate_chart(chart.model_copy(update={"meter": 10})) + assert lv10_validation.valid is False + assert "SUBDIVISION_TOO_FINE_FOR_LEVEL" in { + issue.code for issue in lv10_validation.issues + } + + exported_path = tmp_path / "mixed-sixteenth-twenty-fourth.sm" + exported_path.write_text(export_sm(chart), encoding="utf-8") + reparsed = parse_sm(exported_path) + assert {round(event.beat, 9) for event in reparsed.events} == { + round(sixteenth_beat, 9), + round(twenty_fourth_beat, 9), + } + + +def test_model_filters_candidates_but_preserves_real_hit_point_anchors( + real_speed_sample: RealDatasetSample, +) -> None: + """Confirmed BeatForge hit points are inputs, not model-selectable candidates.""" + + analysis = real_speed_sample.beatforge["analysis"] + sample_rate = int(analysis["original_sample_rate"]) + difficulty = real_speed_sample.training_difficulty + step = 0.25 # This real fixture is Lv.8, whose generator grid is 1/16 notes. + timeline, _changes = _tempo_timeline( + [ + { + "start_sample": 0, + "bpm": analysis["bpm"], + "beat_offset_sample": analysis.get("beat_offset_sample", 0), + } + ], + sample_rate, + ) + + def slot(item: dict[str, Any]) -> int: + chart_sample = int(item.get("chart_sample", item["sample"])) + return int(round(timeline.time_to_beat(chart_sample / sample_rate) / step)) + + # Use the real SPEED structures, but keep the case minimal: three confirmed + # anchors in distinct quantization slots plus two candidate-only events. + anchors: list[dict[str, Any]] = [] + anchor_slots: set[int] = set() + for hit_point in analysis["hit_points"]: + hit_slot = slot(hit_point) + if hit_slot in anchor_slots: + continue + anchors.append(hit_point) + anchor_slots.add(hit_slot) + if len(anchors) == 3: + break + assert len(anchors) == 3 + + candidates_by_id = { + str(candidate["id"]): candidate for candidate in analysis["candidate_events"] + } + anchor_candidate_ids = { + str(anchor["candidate_event_id"]) for anchor in anchors if anchor.get("candidate_event_id") + } + anchor_candidates = [candidates_by_id[candidate_id] for candidate_id in anchor_candidate_ids] + assert len(anchor_candidates) == 3 + + candidate_only: list[dict[str, Any]] = [] + occupied_slots = set(anchor_slots) + for candidate in analysis["candidate_events"]: + candidate_id = str(candidate["id"]) + candidate_slot = slot(candidate) + if candidate_id in anchor_candidate_ids or candidate_slot in occupied_slots: + continue + # This case exercises model-selectable proposals. Accepted candidates + # are rhythm anchors and have their own preservation regression above. + candidate_only.append({**candidate, "status": "uncertain"}) + occupied_slots.add(candidate_slot) + if len(candidate_only) == 2: + break + assert len(candidate_only) == 2 + selected_candidate, rejected_candidate = candidate_only + + predictions = { + str(item["id"]): { + "laneProbabilities": [0.01, 0.01, 0.01, 0.01, 0.01], + "holdProbability": 0.01, + } + for item in anchor_candidates + candidate_only + } + predictions[str(selected_candidate["id"])] = { + "laneProbabilities": [0.99, 0.01, 0.01, 0.01, 0.01], + "holdProbability": 0.01, + } + + chart = generate_chart( + track_id=real_speed_sample.sample_id, + title=real_speed_sample.chart.title, + artist=real_speed_sample.chart.artist, + music=real_speed_sample.chart.music, + duration_sec=float(analysis["duration_sec"]), + sample_rate=sample_rate, + tempo_segments=[ + { + "start_sample": 0, + "bpm": analysis["bpm"], + "beat_offset_sample": analysis.get("beat_offset_sample", 0), + } + ], + candidates=anchor_candidates + candidate_only, + hit_points=anchors, + difficulty=difficulty, + seed=20_260_721, + model_predictions=predictions, + model_provenance={"checkpointSha256": "a" * 64}, + ) + + # Quantization may move an anchor by at most half a grid step. Each of the + # three distinct anchor slots must still be represented by a chart event. + tolerance_sec = (60.0 / float(analysis["bpm"])) * step / 2.0 + 1.0 / sample_rate + event_times = [event.time_sec for event in chart.events] + for anchor in anchors: + anchor_time = int(anchor["chart_sample"]) / sample_rate + assert any(abs(event_time - anchor_time) <= tolerance_sec for event_time in event_times), ( + f"missing confirmed hit-point anchor {anchor['id']} at {anchor_time:.6f}s" + ) + + source_event_ids = {event.source_event_id for event in chart.events} + assert str(selected_candidate["id"]) in source_event_ids + assert str(rejected_candidate["id"]) not in source_event_ids + + +def test_uncertain_aligned_vocal_mora_is_a_required_rhythm_marker() -> None: + sample_rate = 1_000 + tempo_segments = [{"start_sample": 0, "bpm": 120.0, "beat_offset_sample": 0}] + timeline, _changes = _tempo_timeline(tempo_segments, sample_rate) + vocal_marker = { + "id": "aligned-vocal-mora", + "sample": 8_128, + "acoustic_sample": 8_128, + "chart_sample": 8_104, + "lane": "vocals", + "source": "vocals", + "generator": "hubert_ctc", + "event_level": "mora", + "alignment_unit_id": "mora-event:mora-42", + "alignment_unit_index": 42, + "alignment_run_id": "alignment-run", + "mora": "ツ", + "status": "uncertain", + "confidence": 0.405, + "grid_confidence": 0.731, + "salience": 0.405, + } + uncertain_mix = { + **vocal_marker, + "id": "uncertain-mix", + "source": "mix", + "lane": "mix", + "generator": "analysis", + "event_level": "analysis", + "chart_sample": 8_604, + } + rejected_vocal = { + **vocal_marker, + "id": "rejected-vocal-mora", + "status": "rejected", + "chart_sample": 9_104, + } + candidates = [vocal_marker, uncertain_mix, rejected_vocal] + predictions = { + str(candidate["id"]): { + "laneProbabilities": [0.06, 0.45, 0.03, 0.47, 0.28], + "holdProbability": 0.01, + } + for candidate in candidates + } + + points = _source_points( + timeline=timeline, + sample_rate=sample_rate, + duration_sec=12.0, + candidates=candidates, + hit_points=[], + difficulty=8, + model_predictions=predictions, + ) + + assert [point.source_id for point in points] == ["aligned-vocal-mora"] + assert points[0].anchor_priority == 1 + + chart = generate_chart( + track_id="vocal-marker-regression", + title="Vocal marker regression", + artist="BeatForge", + music="fixture.mp3", + duration_sec=12.0, + sample_rate=sample_rate, + tempo_segments=tempo_segments, + candidates=candidates, + hit_points=[], + difficulty=8, + seed=20_260_721, + model_predictions=predictions, + ) + + assert len(chart.events) == 1 + assert chart.events[0].source_event_ids == ["aligned-vocal-mora"] + assert chart.events[0].time_sec == pytest.approx(8.125) + assert chart.optimization is not None + assert chart.optimization["vocal_mora_markers_input"] == 1 + assert chart.optimization["vocal_mora_markers_output"] == 1 + + +def test_same_quantized_slot_is_the_only_hit_point_merge_boundary( + real_speed_sample: RealDatasetSample, +) -> None: + analysis = real_speed_sample.beatforge["analysis"] + sample_rate = int(analysis["original_sample_rate"]) + difficulty = real_speed_sample.training_difficulty + step = 0.25 + timeline, _changes = _tempo_timeline( + [ + { + "start_sample": 0, + "bpm": analysis["bpm"], + "beat_offset_sample": analysis.get("beat_offset_sample", 0), + } + ], + sample_rate, + ) + anchor = dict(analysis["hit_points"][2]) + anchor_beat = ( + round(timeline.time_to_beat(int(anchor["chart_sample"]) / sample_rate) / step) * step + ) + same_slot = { + **anchor, + "id": "same-slot-confirmed-hit", + "sample": int(anchor["chart_sample"]) + 1, + "chart_sample": int(anchor["chart_sample"]) + 1, + "candidate_event_id": None, + } + next_slot_sample = int(round(timeline.beat_to_time(anchor_beat + step) * sample_rate)) + next_slot = { + **anchor, + "id": "next-slot-confirmed-hit", + "sample": next_slot_sample, + "chart_sample": next_slot_sample, + "candidate_event_id": None, + } + + points = _source_points( + timeline=timeline, + sample_rate=sample_rate, + duration_sec=float(analysis["duration_sec"]), + candidates=[], + hit_points=[anchor, same_slot, next_slot], + difficulty=difficulty, + model_predictions=None, + ) + + assert [point.beat for point in points] == [anchor_beat, anchor_beat + step] + assert set(points[0].source_hit_point_ids) == { + str(anchor["id"]), + "same-slot-confirmed-hit", + } + assert points[1].source_hit_point_ids == ("next-slot-confirmed-hit",) + + +def test_point_budget_never_downsamples_real_hit_point_slots( + real_speed_sample: RealDatasetSample, +) -> None: + analysis = real_speed_sample.beatforge["analysis"] + sample_rate = int(analysis["original_sample_rate"]) + difficulty = 1 + timeline, _changes = _tempo_timeline( + [ + { + "start_sample": 0, + "bpm": analysis["bpm"], + "beat_offset_sample": analysis.get("beat_offset_sample", 0), + } + ], + sample_rate, + ) + # This tail of the real SPEED sample has more confirmed quantized slots than + # the Lv.1 point budget. Budgeting may reject candidates, never these anchors. + tail_start = float(analysis["duration_sec"]) * 0.70 + anchors = [ + hit_point + for hit_point in analysis["hit_points"] + if int(hit_point["chart_sample"]) / sample_rate >= tail_start + ] + source_points = _source_points( + timeline=timeline, + sample_rate=sample_rate, + duration_sec=float(analysis["duration_sec"]), + candidates=[], + hit_points=anchors, + difficulty=difficulty, + model_predictions=None, + ) + assert len(source_points) >= 16 + + chart = generate_chart( + track_id=real_speed_sample.sample_id, + title=real_speed_sample.chart.title, + artist=real_speed_sample.chart.artist, + music=real_speed_sample.chart.music, + duration_sec=float(analysis["duration_sec"]), + sample_rate=sample_rate, + tempo_segments=[ + { + "start_sample": 0, + "bpm": analysis["bpm"], + "beat_offset_sample": analysis.get("beat_offset_sample", 0), + } + ], + candidates=[], + hit_points=anchors, + difficulty=difficulty, + seed=20_260_721, + # An empty prediction map selects no candidate-only points and disables + # inferred grid fill, isolating the real confirmed-anchor budget. + model_predictions={}, + ) + + expected_anchor_beats = {point.beat for point in source_points} + assert expected_anchor_beats <= {event.beat for event in chart.events} + + +def test_density_optimizer_downgrades_optional_jump_before_hit_point_anchor( + real_speed_sample: RealDatasetSample, +) -> None: + analysis = real_speed_sample.beatforge["analysis"] + sample_rate = int(analysis["original_sample_rate"]) + difficulty = real_speed_sample.training_difficulty + step = 0.25 + timeline, _changes = _tempo_timeline( + [ + { + "start_sample": 0, + "bpm": analysis["bpm"], + "beat_offset_sample": analysis.get("beat_offset_sample", 0), + } + ], + sample_rate, + ) + first_beat = 8.0 + candidate_ids: list[str] = [] + events: list[ChartEvent] = [] + for index, candidate in enumerate(analysis["candidate_events"][:7]): + beat = first_beat + index * step + candidate_id = str(candidate["id"]) + candidate_ids.append(candidate_id) + events.append( + ChartEvent( + time_sec=timeline.beat_to_time(beat), + beat=beat, + measure=int(beat // 4), + subdivision=16, + row_index=int(round((beat % 4) * 4)), + notes=[ + ChartNote(lane=0, source="candidate", confidence=1.0 - index * 0.01), + ChartNote(lane=4, source="candidate", confidence=0.9 - index * 0.01), + ], + source_event_id=candidate_id, + source_event_ids=[candidate_id], + anchor_priority=0, + ) + ) + + anchor_beat = first_beat + 7 * step + anchor_id = "density-protected-hit" + events.append( + ChartEvent( + time_sec=timeline.beat_to_time(anchor_beat), + beat=anchor_beat, + measure=int(anchor_beat // 4), + subdivision=16, + row_index=int(round((anchor_beat % 4) * 4)), + notes=[ChartNote(lane=2, source="hit_point", confidence=1.0)], + source_event_id=anchor_id, + source_hit_point_ids=[anchor_id], + anchor_priority=2, + ) + ) + + optimized, report = optimize_events(events, difficulty) + + assert any(event.source_hit_point_ids == [anchor_id] for event in optimized) + retained_optional_ids = { + source_id for event in optimized for source_id in event.source_event_ids + } + assert retained_optional_ids == set(candidate_ids) + assert sum(len(event.notes) == 1 for event in optimized[:-1]) == 1 + assert report.simultaneous_notes_removed == 1 + assert report.density_events_removed == 0 + + accepted_events = [ + event.model_copy(update={"anchor_priority": 1}) for event in events[:-1] + ] + accepted_optimized, accepted_report = optimize_events( + [*accepted_events, events[-1]], difficulty + ) + assert len(accepted_optimized) == len(events) + assert accepted_report.density_events_removed == 0 + + +def test_lv8_optimizer_breaks_disjoint_jump_chain_without_losing_rows() -> None: + def jump_event(beat: float, lanes: tuple[int, int], source_id: str) -> ChartEvent: + return ChartEvent( + time_sec=beat * 0.5, + beat=beat, + measure=int(beat // 4), + subdivision=8, + row_index=int(round((beat % 4) * 2)), + notes=[ + ChartNote(lane=lanes[0], source="candidate", confidence=0.9), + ChartNote(lane=lanes[1], source="candidate", confidence=0.8), + ], + source_event_id=source_id, + source_event_ids=[source_id], + anchor_priority=1, + ) + + raw_events = [ + jump_event(4.0, (0, 1), "first-jump"), + jump_event(4.5, (2, 3), "reposition-jump"), + jump_event(8.0, (3, 4), "isolated-jump"), + ] + raw_chart = ChartDocument( + id="jump-chain-validator", + title="Jump chain validator", + mode="pump-single", + lane_count=5, + meter=8, + bpm=120.0, + duration_sec=5.0, + measure_count=3, + tempo_map=[{"beat": 0.0, "bpm": 120.0, "time_sec": 0.0}], + events=raw_events, + ) + + raw_validation = validate_chart(raw_chart) + assert "DISJOINT_JUMP_TRANSITION" in { + issue.code for issue in raw_validation.issues + } + + optimized, report = optimize_events(raw_events, 8, bpm=120.0) + + assert [len(event.notes) for event in optimized] == [2, 1, 2] + assert [event.source_event_id for event in optimized] == [ + "first-jump", + "reposition-jump", + "isolated-jump", + ] + assert report.simultaneous_notes_removed == 1 + optimized_chart = raw_chart.model_copy(update={"events": optimized}) + assert "DISJOINT_JUMP_TRANSITION" not in { + issue.code for issue in validate_chart(optimized_chart).issues + } + + +def test_lv8_optimizer_repairs_forced_spin_without_losing_vocal_rows() -> None: + times = [ + 39.3834140167695, + 39.49969308653694, + 39.61597215630438, + 39.84853029583927, + 39.964809365606705, + 40.19736750514159, + ] + beats = [84.5, 84.75, 85.0, 85.5, 85.75, 86.25] + # User-reported 5(R), 3(L), 4(R), 1(L), 2(R), 4(L). The final + # left-foot right-up step places L=right-up/R=left-up and faces backward. + lanes = [4, 2, 3, 0, 1, 3] + stale_feet = ["right", "left", "right", "left", "left", "right"] + source_ids = [f"aligned-vocal-{index}" for index in range(len(lanes))] + events = [ + ChartEvent( + time_sec=time_sec, + beat=beat, + measure=int(beat // 4), + subdivision=16, + row_index=int(round((beat % 4) * 4)), + notes=[ + ChartNote( + lane=lane, + source="vocals", + confidence=0.9, + foot=foot, + ) + ], + source_event_id=source_id, + source_event_ids=[source_id], + anchor_priority=1, + ) + for time_sec, beat, lane, foot, source_id in zip( + times, beats, lanes, stale_feet, source_ids, strict=True + ) + ] + raw_chart = ChartDocument( + id="forced-spin-regression", + title="Forced spin regression", + source_group="BEATFORGE_GENERATED", + mode="pump-single", + lane_count=5, + meter=8, + bpm=129.0, + duration_sec=42.0, + measure_count=22, + tempo_map=[{"beat": 0.0, "bpm": 129.0, "time_sec": 0.0}], + events=events, + generator="local_chart_transformer", + spin_enabled=False, + ) + + raw_analysis = analyze_no_spin_footwork(events) + assert raw_analysis.full_step_reachable is False + assert raw_analysis.violations[0].beat == 86.25 + raw_issue = next( + issue + for issue in validate_chart(raw_chart).issues + if issue.code == "NO_FULL_ALTERNATING_PATH" + ) + assert raw_issue.time_sec == pytest.approx(times[-1]) + + optimized, report = optimize_events(events, 8, bpm=129.0) + + assert len(optimized) == len(events) + assert [event.time_sec for event in optimized] == times + assert [event.source_event_id for event in optimized] == source_ids + assert [event.source_event_ids for event in optimized] == [[value] for value in source_ids] + assert report.footwork_lanes_reassigned == 1 + assert [event.notes[0].foot for event in optimized] == [ + "right", + "left", + "right", + "left", + "right", + "left", + ] + assert [event.notes[0].lane for event in optimized] == [4, 2, 3, 0, 1, 2] + assert analyze_no_spin_footwork(optimized).full_step_reachable is True + optimized_chart = raw_chart.model_copy(update={"events": optimized}) + assert "NO_FULL_ALTERNATING_PATH" not in { + issue.code for issue in validate_chart(optimized_chart).issues + } + + +def test_no_spin_solver_keeps_legal_135_degree_crossovers() -> None: + # DL, UL, center, UR requires a deep crossover for either starting foot, + # but never faces backward. A 90-degree-only rule would reject real SPEED + # technique; the no-spin limit intentionally includes +/-135 degrees. + events = [ + ChartEvent( + time_sec=index * 0.2, + beat=index * 0.25, + measure=0, + subdivision=16, + row_index=index, + notes=[ChartNote(lane=lane)], + ) + for index, lane in enumerate((0, 1, 2, 3)) + ] + + analysis = analyze_no_spin_footwork(events) + + assert analysis.full_step_reachable is True + assert analysis.max_abs_heading == pytest.approx(135.0) + + +def test_corpus_transitions_do_not_bridge_across_a_jump() -> None: + events = [ + ChartEvent( + time_sec=0.0, + beat=0.0, + measure=0, + notes=[ChartNote(lane=4)], + ), + ChartEvent( + time_sec=0.25, + beat=0.5, + measure=0, + notes=[ChartNote(lane=1), ChartNote(lane=2)], + ), + ChartEvent( + time_sec=0.5, + beat=1.0, + measure=0, + notes=[ChartNote(lane=3)], + ), + ] + chart = ChartDocument( + id="jump-transition-boundary", + title="Jump transition boundary", + mode="pump-single", + lane_count=5, + meter=14, + bpm=120.0, + duration_sec=1.0, + measure_count=1, + tempo_map=[{"beat": 0.0, "bpm": 120.0, "time_sec": 0.0}], + events=events, + ) + + transitions = corpus_statistics([chart]).lane_transition_probabilities + + assert transitions == [[0.2] * 5 for _ in range(5)] + + +def test_hit_point_replacement_keeps_candidate_prediction_provenance( + real_speed_sample: RealDatasetSample, +) -> None: + analysis = real_speed_sample.beatforge["analysis"] + predictions = { + str(item["id"]): { + "laneProbabilities": [0.9, 0.1, 0.1, 0.1, 0.1], + "holdProbability": 0.1, + } + for item in analysis["candidate_events"] + } + timeline, _changes = _tempo_timeline( + [ + { + "start_sample": 0, + "bpm": analysis["bpm"], + "beat_offset_sample": analysis.get("beat_offset_sample", 0), + } + ], + int(analysis["original_sample_rate"]), + ) + + points = _source_points( + timeline=timeline, + sample_rate=int(analysis["original_sample_rate"]), + duration_sec=float(analysis["duration_sec"]), + candidates=analysis["candidate_events"], + hit_points=analysis["hit_points"], + difficulty=real_speed_sample.training_difficulty, + model_predictions=predictions, + ) + + probability_points = [point for point in points if point.lane_probabilities is not None] + assert any(point.source == "hit_point" for point in probability_points) + assert all(point.source_id in predictions for point in probability_points) + + +def test_route_uses_local_checkpoint_and_records_real_training_provenance( + client: TestClient, + real_speed_sample: RealDatasetSample, + tiny_real_checkpoint: TrainingResult, + monkeypatch: pytest.MonkeyPatch, +) -> None: + _use_route_storage(monkeypatch, tiny_real_checkpoint.checkpoint_path.parents[2]) + track_id = _seed_real_speed_track(real_speed_sample) + + payload = _generate(client, track_id, use_model=True) + + model_status = payload["referenceCorpus"]["model"] + chart = payload["chart"] + provenance = chart["modelProvenance"] + assert model_status == {"requested": True, "available": True, "used": True} + assert chart["generator"] == "local_chart_transformer" + assert provenance["schemaVersion"] == "beatforge.chart-transformer.checkpoint.v1" + assert provenance["architecture"] == "beatforge.chart-transformer.encoder.v1" + assert provenance["datasetFingerprint"] == tiny_real_checkpoint.metadata["datasetFingerprint"] + assert provenance["sampleCount"] == tiny_real_checkpoint.metadata["sampleCount"] + assert provenance["realDataOnly"] is True + assert ( + provenance["checkpointSha256"] + == hashlib.sha256(tiny_real_checkpoint.checkpoint_path.read_bytes()).hexdigest() + ) + real_candidate_ids = { + item["id"] for item in real_speed_sample.beatforge["analysis"]["candidate_events"] + } + assert chart["events"] + assert all(event["sourceEventId"] in real_candidate_ids for event in chart["events"]) + + rule_payload = _generate(client, track_id, use_model=False) + assert rule_payload["chart"]["generator"] == "real_corpus_profile_rules" + assert rule_payload["generationId"] != payload["generationId"] + saved_model_chart = chart_routes._load_generated(track_id, payload["generationId"]) + assert saved_model_chart.generator == "local_chart_transformer" + assert saved_model_chart.model_provenance == provenance + + torch = pytest.importorskip("torch") + checkpoint_payload = torch.load( + tiny_real_checkpoint.checkpoint_path, map_location="cpu", weights_only=True + ) + checkpoint_payload["model_state_dict"]["lane_head.bias"][0] += 0.125 + torch.save(checkpoint_payload, tiny_real_checkpoint.checkpoint_path) + chart_routes._load_local_chart_model.cache_clear() + changed_payload = _generate(client, track_id, use_model=True) + changed_provenance = changed_payload["chart"]["modelProvenance"] + assert changed_provenance["checkpointSha256"] != provenance["checkpointSha256"] + assert changed_payload["generationId"] != payload["generationId"] + assert ( + chart_routes._load_generated(track_id, payload["generationId"]).model_provenance + == provenance + ) + + +def test_route_falls_back_to_rules_when_checkpoint_is_absent( + client: TestClient, + real_speed_sample: RealDatasetSample, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + settings = _use_route_storage(monkeypatch, tmp_path / "fallback-storage") + assert not (settings.chart_models_dir / "chart-transformer.pt").exists() + track_id = _seed_real_speed_track(real_speed_sample) + + first = _generate(client, track_id, use_model=True) + second = _generate(client, track_id, use_model=True) + + assert first == second + assert first["referenceCorpus"]["model"] == { + "requested": True, + "available": False, + "used": False, + } + assert first["chart"]["generator"] == "real_corpus_profile_rules" + assert first["chart"]["modelProvenance"] is None + + +def test_route_rejects_rule_generation_without_the_real_speed_corpus( + client: TestClient, + real_speed_sample: RealDatasetSample, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + _use_route_storage(monkeypatch, tmp_path / "empty-corpus-storage") + empty_corpus = tmp_path / "empty-speed-corpus" + empty_corpus.mkdir() + monkeypatch.setenv("BEATFORGE_SPEED_CHARTS_DIR", str(empty_corpus)) + chart_routes._library_for_root.cache_clear() + chart_routes._statistics_for_root.cache_clear() + track_id = _seed_real_speed_track(real_speed_sample) + + response = client.post( + f"/api/tracks/{track_id}/chart/generate", + json={"difficulty": 7, "useLocalModel": False, "seed": 20_260_721}, + ) + + assert response.status_code == 404 + assert response.json()["error"]["code"] == "REFERENCE_CORPUS_NOT_FOUND" diff --git a/apps/api/uv.lock b/apps/api/uv.lock new file mode 100644 index 0000000..540a32e --- /dev/null +++ b/apps/api/uv.lock @@ -0,0 +1,2307 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version < '3.13'", +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "antlr4-python3-runtime" +version = "4.9.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/38/7859ff46355f76f8d19459005ca000b6e7012f2f1ca597746cbcd1fbfe5e/antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b", size = 117034, upload-time = "2021-11-06T17:52:23.524Z" } + +[[package]] +name = "anyio" +version = "4.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, +] + +[[package]] +name = "audioop-lts" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/53/946db57842a50b2da2e0c1e34bd37f36f5aadba1a929a3971c5d7841dbca/audioop_lts-0.2.2.tar.gz", hash = "sha256:64d0c62d88e67b98a1a5e71987b7aa7b5bcffc7dcee65b635823dbdd0a8dbbd0", size = 30686, upload-time = "2025-08-05T16:43:17.409Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/d4/94d277ca941de5a507b07f0b592f199c22454eeaec8f008a286b3fbbacd6/audioop_lts-0.2.2-cp313-abi3-macosx_10_13_universal2.whl", hash = "sha256:fd3d4602dc64914d462924a08c1a9816435a2155d74f325853c1f1ac3b2d9800", size = 46523, upload-time = "2025-08-05T16:42:20.836Z" }, + { url = "https://files.pythonhosted.org/packages/f8/5a/656d1c2da4b555920ce4177167bfeb8623d98765594af59702c8873f60ec/audioop_lts-0.2.2-cp313-abi3-macosx_10_13_x86_64.whl", hash = "sha256:550c114a8df0aafe9a05442a1162dfc8fec37e9af1d625ae6060fed6e756f303", size = 27455, upload-time = "2025-08-05T16:42:22.283Z" }, + { url = "https://files.pythonhosted.org/packages/1b/83/ea581e364ce7b0d41456fb79d6ee0ad482beda61faf0cab20cbd4c63a541/audioop_lts-0.2.2-cp313-abi3-macosx_11_0_arm64.whl", hash = "sha256:9a13dc409f2564de15dd68be65b462ba0dde01b19663720c68c1140c782d1d75", size = 26997, upload-time = "2025-08-05T16:42:23.849Z" }, + { url = "https://files.pythonhosted.org/packages/b8/3b/e8964210b5e216e5041593b7d33e97ee65967f17c282e8510d19c666dab4/audioop_lts-0.2.2-cp313-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51c916108c56aa6e426ce611946f901badac950ee2ddaf302b7ed35d9958970d", size = 85844, upload-time = "2025-08-05T16:42:25.208Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2e/0a1c52faf10d51def20531a59ce4c706cb7952323b11709e10de324d6493/audioop_lts-0.2.2-cp313-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47eba38322370347b1c47024defbd36374a211e8dd5b0dcbce7b34fdb6f8847b", size = 85056, upload-time = "2025-08-05T16:42:26.559Z" }, + { url = "https://files.pythonhosted.org/packages/75/e8/cd95eef479656cb75ab05dfece8c1f8c395d17a7c651d88f8e6e291a63ab/audioop_lts-0.2.2-cp313-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba7c3a7e5f23e215cb271516197030c32aef2e754252c4c70a50aaff7031a2c8", size = 93892, upload-time = "2025-08-05T16:42:27.902Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1e/a0c42570b74f83efa5cca34905b3eef03f7ab09fe5637015df538a7f3345/audioop_lts-0.2.2-cp313-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:def246fe9e180626731b26e89816e79aae2276f825420a07b4a647abaa84becc", size = 96660, upload-time = "2025-08-05T16:42:28.9Z" }, + { url = "https://files.pythonhosted.org/packages/50/d5/8a0ae607ca07dbb34027bac8db805498ee7bfecc05fd2c148cc1ed7646e7/audioop_lts-0.2.2-cp313-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e160bf9df356d841bb6c180eeeea1834085464626dc1b68fa4e1d59070affdc3", size = 79143, upload-time = "2025-08-05T16:42:29.929Z" }, + { url = "https://files.pythonhosted.org/packages/12/17/0d28c46179e7910bfb0bb62760ccb33edb5de973052cb2230b662c14ca2e/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4b4cd51a57b698b2d06cb9993b7ac8dfe89a3b2878e96bc7948e9f19ff51dba6", size = 84313, upload-time = "2025-08-05T16:42:30.949Z" }, + { url = "https://files.pythonhosted.org/packages/84/ba/bd5d3806641564f2024e97ca98ea8f8811d4e01d9b9f9831474bc9e14f9e/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4a53aa7c16a60a6857e6b0b165261436396ef7293f8b5c9c828a3a203147ed4a", size = 93044, upload-time = "2025-08-05T16:42:31.959Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5e/435ce8d5642f1f7679540d1e73c1c42d933331c0976eb397d1717d7f01a3/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:3fc38008969796f0f689f1453722a0f463da1b8a6fbee11987830bfbb664f623", size = 78766, upload-time = "2025-08-05T16:42:33.302Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3b/b909e76b606cbfd53875693ec8c156e93e15a1366a012f0b7e4fb52d3c34/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_s390x.whl", hash = "sha256:15ab25dd3e620790f40e9ead897f91e79c0d3ce65fe193c8ed6c26cffdd24be7", size = 87640, upload-time = "2025-08-05T16:42:34.854Z" }, + { url = "https://files.pythonhosted.org/packages/30/e7/8f1603b4572d79b775f2140d7952f200f5e6c62904585d08a01f0a70393a/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:03f061a1915538fd96272bac9551841859dbb2e3bf73ebe4a23ef043766f5449", size = 86052, upload-time = "2025-08-05T16:42:35.839Z" }, + { url = "https://files.pythonhosted.org/packages/b5/96/c37846df657ccdda62ba1ae2b6534fa90e2e1b1742ca8dcf8ebd38c53801/audioop_lts-0.2.2-cp313-abi3-win32.whl", hash = "sha256:3bcddaaf6cc5935a300a8387c99f7a7fbbe212a11568ec6cf6e4bc458c048636", size = 26185, upload-time = "2025-08-05T16:42:37.04Z" }, + { url = "https://files.pythonhosted.org/packages/34/a5/9d78fdb5b844a83da8a71226c7bdae7cc638861085fff7a1d707cb4823fa/audioop_lts-0.2.2-cp313-abi3-win_amd64.whl", hash = "sha256:a2c2a947fae7d1062ef08c4e369e0ba2086049a5e598fda41122535557012e9e", size = 30503, upload-time = "2025-08-05T16:42:38.427Z" }, + { url = "https://files.pythonhosted.org/packages/34/25/20d8fde083123e90c61b51afb547bb0ea7e77bab50d98c0ab243d02a0e43/audioop_lts-0.2.2-cp313-abi3-win_arm64.whl", hash = "sha256:5f93a5db13927a37d2d09637ccca4b2b6b48c19cd9eda7b17a2e9f77edee6a6f", size = 24173, upload-time = "2025-08-05T16:42:39.704Z" }, + { url = "https://files.pythonhosted.org/packages/58/a7/0a764f77b5c4ac58dc13c01a580f5d32ae8c74c92020b961556a43e26d02/audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:73f80bf4cd5d2ca7814da30a120de1f9408ee0619cc75da87d0641273d202a09", size = 47096, upload-time = "2025-08-05T16:42:40.684Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ed/ebebedde1a18848b085ad0fa54b66ceb95f1f94a3fc04f1cd1b5ccb0ed42/audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:106753a83a25ee4d6f473f2be6b0966fc1c9af7e0017192f5531a3e7463dce58", size = 27748, upload-time = "2025-08-05T16:42:41.992Z" }, + { url = "https://files.pythonhosted.org/packages/cb/6e/11ca8c21af79f15dbb1c7f8017952ee8c810c438ce4e2b25638dfef2b02c/audioop_lts-0.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fbdd522624141e40948ab3e8cdae6e04c748d78710e9f0f8d4dae2750831de19", size = 27329, upload-time = "2025-08-05T16:42:42.987Z" }, + { url = "https://files.pythonhosted.org/packages/84/52/0022f93d56d85eec5da6b9da6a958a1ef09e80c39f2cc0a590c6af81dcbb/audioop_lts-0.2.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:143fad0311e8209ece30a8dbddab3b65ab419cbe8c0dde6e8828da25999be911", size = 92407, upload-time = "2025-08-05T16:42:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/87/1d/48a889855e67be8718adbc7a01f3c01d5743c325453a5e81cf3717664aad/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfbbc74ec68a0fd08cfec1f4b5e8cca3d3cd7de5501b01c4b5d209995033cde9", size = 91811, upload-time = "2025-08-05T16:42:45.325Z" }, + { url = "https://files.pythonhosted.org/packages/98/a6/94b7213190e8077547ffae75e13ed05edc488653c85aa5c41472c297d295/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cfcac6aa6f42397471e4943e0feb2244549db5c5d01efcd02725b96af417f3fe", size = 100470, upload-time = "2025-08-05T16:42:46.468Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e9/78450d7cb921ede0cfc33426d3a8023a3bda755883c95c868ee36db8d48d/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:752d76472d9804ac60f0078c79cdae8b956f293177acd2316cd1e15149aee132", size = 103878, upload-time = "2025-08-05T16:42:47.576Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e2/cd5439aad4f3e34ae1ee852025dc6aa8f67a82b97641e390bf7bd9891d3e/audioop_lts-0.2.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:83c381767e2cc10e93e40281a04852facc4cd9334550e0f392f72d1c0a9c5753", size = 84867, upload-time = "2025-08-05T16:42:49.003Z" }, + { url = "https://files.pythonhosted.org/packages/68/4b/9d853e9076c43ebba0d411e8d2aa19061083349ac695a7d082540bad64d0/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c0022283e9556e0f3643b7c3c03f05063ca72b3063291834cca43234f20c60bb", size = 90001, upload-time = "2025-08-05T16:42:50.038Z" }, + { url = "https://files.pythonhosted.org/packages/58/26/4bae7f9d2f116ed5593989d0e521d679b0d583973d203384679323d8fa85/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a2d4f1513d63c795e82948e1305f31a6d530626e5f9f2605408b300ae6095093", size = 99046, upload-time = "2025-08-05T16:42:51.111Z" }, + { url = "https://files.pythonhosted.org/packages/b2/67/a9f4fb3e250dda9e9046f8866e9fa7d52664f8985e445c6b4ad6dfb55641/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:c9c8e68d8b4a56fda8c025e538e639f8c5953f5073886b596c93ec9b620055e7", size = 84788, upload-time = "2025-08-05T16:42:52.198Z" }, + { url = "https://files.pythonhosted.org/packages/70/f7/3de86562db0121956148bcb0fe5b506615e3bcf6e63c4357a612b910765a/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:96f19de485a2925314f5020e85911fb447ff5fbef56e8c7c6927851b95533a1c", size = 94472, upload-time = "2025-08-05T16:42:53.59Z" }, + { url = "https://files.pythonhosted.org/packages/f1/32/fd772bf9078ae1001207d2df1eef3da05bea611a87dd0e8217989b2848fa/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e541c3ef484852ef36545f66209444c48b28661e864ccadb29daddb6a4b8e5f5", size = 92279, upload-time = "2025-08-05T16:42:54.632Z" }, + { url = "https://files.pythonhosted.org/packages/4f/41/affea7181592ab0ab560044632571a38edaf9130b84928177823fbf3176a/audioop_lts-0.2.2-cp313-cp313t-win32.whl", hash = "sha256:d5e73fa573e273e4f2e5ff96f9043858a5e9311e94ffefd88a3186a910c70917", size = 26568, upload-time = "2025-08-05T16:42:55.627Z" }, + { url = "https://files.pythonhosted.org/packages/28/2b/0372842877016641db8fc54d5c88596b542eec2f8f6c20a36fb6612bf9ee/audioop_lts-0.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9191d68659eda01e448188f60364c7763a7ca6653ed3f87ebb165822153a8547", size = 30942, upload-time = "2025-08-05T16:42:56.674Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/baf2b9cc7e96c179bb4a54f30fcd83e6ecb340031bde68f486403f943768/audioop_lts-0.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c174e322bb5783c099aaf87faeb240c8d210686b04bd61dfd05a8e5a83d88969", size = 24603, upload-time = "2025-08-05T16:42:57.571Z" }, + { url = "https://files.pythonhosted.org/packages/5c/73/413b5a2804091e2c7d5def1d618e4837f1cb82464e230f827226278556b7/audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f9ee9b52f5f857fbaf9d605a360884f034c92c1c23021fb90b2e39b8e64bede6", size = 47104, upload-time = "2025-08-05T16:42:58.518Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/daa3308dc6593944410c2c68306a5e217f5c05b70a12e70228e7dd42dc5c/audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:49ee1a41738a23e98d98b937a0638357a2477bc99e61b0f768a8f654f45d9b7a", size = 27754, upload-time = "2025-08-05T16:43:00.132Z" }, + { url = "https://files.pythonhosted.org/packages/4e/86/c2e0f627168fcf61781a8f72cab06b228fe1da4b9fa4ab39cfb791b5836b/audioop_lts-0.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5b00be98ccd0fc123dcfad31d50030d25fcf31488cde9e61692029cd7394733b", size = 27332, upload-time = "2025-08-05T16:43:01.666Z" }, + { url = "https://files.pythonhosted.org/packages/c7/bd/35dce665255434f54e5307de39e31912a6f902d4572da7c37582809de14f/audioop_lts-0.2.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6d2e0f9f7a69403e388894d4ca5ada5c47230716a03f2847cfc7bd1ecb589d6", size = 92396, upload-time = "2025-08-05T16:43:02.991Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d2/deeb9f51def1437b3afa35aeb729d577c04bcd89394cb56f9239a9f50b6f/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9b0b8a03ef474f56d1a842af1a2e01398b8f7654009823c6d9e0ecff4d5cfbf", size = 91811, upload-time = "2025-08-05T16:43:04.096Z" }, + { url = "https://files.pythonhosted.org/packages/76/3b/09f8b35b227cee28cc8231e296a82759ed80c1a08e349811d69773c48426/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b267b70747d82125f1a021506565bdc5609a2b24bcb4773c16d79d2bb260bbd", size = 100483, upload-time = "2025-08-05T16:43:05.085Z" }, + { url = "https://files.pythonhosted.org/packages/0b/15/05b48a935cf3b130c248bfdbdea71ce6437f5394ee8533e0edd7cfd93d5e/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0337d658f9b81f4cd0fdb1f47635070cc084871a3d4646d9de74fdf4e7c3d24a", size = 103885, upload-time = "2025-08-05T16:43:06.197Z" }, + { url = "https://files.pythonhosted.org/packages/83/80/186b7fce6d35b68d3d739f228dc31d60b3412105854edb975aa155a58339/audioop_lts-0.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:167d3b62586faef8b6b2275c3218796b12621a60e43f7e9d5845d627b9c9b80e", size = 84899, upload-time = "2025-08-05T16:43:07.291Z" }, + { url = "https://files.pythonhosted.org/packages/49/89/c78cc5ac6cb5828f17514fb12966e299c850bc885e80f8ad94e38d450886/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0d9385e96f9f6da847f4d571ce3cb15b5091140edf3db97276872647ce37efd7", size = 89998, upload-time = "2025-08-05T16:43:08.335Z" }, + { url = "https://files.pythonhosted.org/packages/4c/4b/6401888d0c010e586c2ca50fce4c903d70a6bb55928b16cfbdfd957a13da/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:48159d96962674eccdca9a3df280e864e8ac75e40a577cc97c5c42667ffabfc5", size = 99046, upload-time = "2025-08-05T16:43:09.367Z" }, + { url = "https://files.pythonhosted.org/packages/de/f8/c874ca9bb447dae0e2ef2e231f6c4c2b0c39e31ae684d2420b0f9e97ee68/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8fefe5868cd082db1186f2837d64cfbfa78b548ea0d0543e9b28935ccce81ce9", size = 84843, upload-time = "2025-08-05T16:43:10.749Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c0/0323e66f3daebc13fd46b36b30c3be47e3fc4257eae44f1e77eb828c703f/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:58cf54380c3884fb49fdd37dfb7a772632b6701d28edd3e2904743c5e1773602", size = 94490, upload-time = "2025-08-05T16:43:12.131Z" }, + { url = "https://files.pythonhosted.org/packages/98/6b/acc7734ac02d95ab791c10c3f17ffa3584ccb9ac5c18fd771c638ed6d1f5/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:088327f00488cdeed296edd9215ca159f3a5a5034741465789cad403fcf4bec0", size = 92297, upload-time = "2025-08-05T16:43:13.139Z" }, + { url = "https://files.pythonhosted.org/packages/13/c3/c3dc3f564ce6877ecd2a05f8d751b9b27a8c320c2533a98b0c86349778d0/audioop_lts-0.2.2-cp314-cp314t-win32.whl", hash = "sha256:068aa17a38b4e0e7de771c62c60bbca2455924b67a8814f3b0dee92b5820c0b3", size = 27331, upload-time = "2025-08-05T16:43:14.19Z" }, + { url = "https://files.pythonhosted.org/packages/72/bb/b4608537e9ffcb86449091939d52d24a055216a36a8bf66b936af8c3e7ac/audioop_lts-0.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a5bf613e96f49712073de86f20dbdd4014ca18efd4d34ed18c75bd808337851b", size = 31697, upload-time = "2025-08-05T16:43:15.193Z" }, + { url = "https://files.pythonhosted.org/packages/f6/22/91616fe707a5c5510de2cac9b046a30defe7007ba8a0c04f9c08f27df312/audioop_lts-0.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:b492c3b040153e68b9fdaff5913305aaaba5bb433d8a7f73d5cf6a64ed3cc1dd", size = 25206, upload-time = "2025-08-05T16:43:16.444Z" }, +] + +[[package]] +name = "audioread" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "standard-aifc", marker = "python_full_version >= '3.13'" }, + { name = "standard-sunau", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/4a/874ecf9b472f998130c2b5e145dcdb9f6131e84786111489103b66772143/audioread-3.1.0.tar.gz", hash = "sha256:1c4ab2f2972764c896a8ac61ac53e261c8d29f0c6ccd652f84e18f08a4cab190", size = 20082, upload-time = "2025-10-26T19:44:13.484Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/16/fbe8e1e185a45042f7cd3a282def5bb8d95bb69ab9e9ef6a5368aa17e426/audioread-3.1.0-py3-none-any.whl", hash = "sha256:b30d1df6c5d3de5dcef0fb0e256f6ea17bdcf5f979408df0297d8a408e2971b4", size = 23143, upload-time = "2025-10-26T19:44:12.016Z" }, +] + +[[package]] +name = "beatforge-api" +version = "0.7.1" +source = { editable = "." } +dependencies = [ + { name = "fastapi" }, + { name = "librosa" }, + { name = "numpy" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "python-dotenv" }, + { name = "python-multipart" }, + { name = "scipy" }, + { name = "soundfile" }, + { name = "sqlalchemy" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[package.optional-dependencies] +accurate = [ + { name = "demucs" }, + { name = "torch" }, + { name = "torchaudio" }, +] +chart-ml = [ + { name = "torch" }, +] +dev = [ + { name = "build" }, + { name = "hatchling" }, + { name = "httpx" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "build", marker = "extra == 'dev'", specifier = "==1.2.2.post1" }, + { name = "demucs", marker = "extra == 'accurate'", specifier = "==4.0.1" }, + { name = "fastapi", specifier = "==0.115.12" }, + { name = "hatchling", marker = "extra == 'dev'", specifier = "==1.27.0" }, + { name = "httpx", marker = "extra == 'dev'", specifier = "==0.28.1" }, + { name = "librosa", specifier = "==0.11.0" }, + { name = "numpy", specifier = "==2.2.6" }, + { name = "pydantic", specifier = "==2.11.5" }, + { name = "pydantic-settings", specifier = "==2.9.1" }, + { name = "pytest", marker = "extra == 'dev'", specifier = "==8.3.5" }, + { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = "==0.26.0" }, + { name = "python-dotenv", specifier = "==1.1.0" }, + { name = "python-multipart", specifier = "==0.0.20" }, + { name = "ruff", marker = "extra == 'dev'", specifier = "==0.11.11" }, + { name = "scipy", specifier = "==1.15.3" }, + { name = "soundfile", specifier = "==0.13.1" }, + { name = "sqlalchemy", specifier = "==2.0.41" }, + { name = "torch", marker = "extra == 'accurate'", specifier = "==2.13.0" }, + { name = "torch", marker = "extra == 'chart-ml'", specifier = "==2.13.0" }, + { name = "torchaudio", marker = "extra == 'accurate'", specifier = "==2.11.0" }, + { name = "uvicorn", extras = ["standard"], specifier = "==0.34.2" }, +] +provides-extras = ["dev", "accurate", "chart-ml"] + +[[package]] +name = "build" +version = "1.2.2.post1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "os_name == 'nt'" }, + { name = "packaging" }, + { name = "pyproject-hooks" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/46/aeab111f8e06793e4f0e421fcad593d547fb8313b50990f31681ee2fb1ad/build-1.2.2.post1.tar.gz", hash = "sha256:b36993e92ca9375a219c99e606a122ff365a760a2d4bba0caa09bd5278b608b7", size = 46701, upload-time = "2024-10-06T17:22:25.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/c2/80633736cd183ee4a62107413def345f7e6e3c01563dbca1417363cf957e/build-1.2.2.post1-py3-none-any.whl", hash = "sha256:1d61c0887fa860c01971625baae8bdd338e517b836a2f70dd1f7aa3a6b2fc5b5", size = 22950, upload-time = "2024-10-06T17:22:23.299Z" }, +] + +[[package]] +name = "certifi" +version = "2026.6.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, +] + +[[package]] +name = "cffi" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/67/85c89a59ba36a671e79638f44d466749f08179266a57e4f2ffdf92174072/cffi-2.1.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc", size = 183845, upload-time = "2026-07-06T21:32:26.32Z" }, + { url = "https://files.pythonhosted.org/packages/ea/dd/e3b0baa2d3d6a857ac72b7efbf18e32e487c9cdafcc13049ad765495b15e/cffi-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7", size = 184186, upload-time = "2026-07-06T21:32:28.025Z" }, + { url = "https://files.pythonhosted.org/packages/65/68/9f3ef890cf3c6ab97bd531c5677f67613d302165d16f8142b2811782a614/cffi-2.1.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93", size = 211892, upload-time = "2026-07-06T21:32:29.565Z" }, + { url = "https://files.pythonhosted.org/packages/22/d7/1a74539db16d8bfd839ff1515948948efbb162e574650fd3d846896eea95/cffi-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2", size = 218793, upload-time = "2026-07-06T21:32:30.951Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d1/9a5b7169499e8e8d8e636de70b97ac7c9447104d2ff1a2cd94790cea5162/cffi-2.1.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c", size = 205737, upload-time = "2026-07-06T21:32:32.216Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b0/e131a9c41f10607926278453d9596163594fe1c4ebc46efe3b5e5b34eb84/cffi-2.1.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f", size = 204909, upload-time = "2026-07-06T21:32:33.655Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d2/4398416cd699b35167947c6e22aca52c47e69ad5695073c9f1f2c52e04aa/cffi-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565", size = 217883, upload-time = "2026-07-06T21:32:35.173Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a5/d4fe77b589e5e82d43ebc809bf2e6474afe8e48e32ea050b9357645b6471/cffi-2.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c", size = 221251, upload-time = "2026-07-06T21:32:36.527Z" }, + { url = "https://files.pythonhosted.org/packages/22/f0/a2fc43084c0433caf7f461bccc013e28f848d04ee1c5ed7fce71423cf4d9/cffi-2.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02", size = 214250, upload-time = "2026-07-06T21:32:37.852Z" }, + { url = "https://files.pythonhosted.org/packages/04/8c/b925975448cf20634a9fbd5efceb807219db452653648d2897c0989cab2d/cffi-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e", size = 219441, upload-time = "2026-07-06T21:32:39.146Z" }, + { url = "https://files.pythonhosted.org/packages/eb/da/5c4918a2d61d86fa927d716cb3d8e4626ef8dc8f605a599d32f33897f59a/cffi-2.1.0-cp311-cp311-win32.whl", hash = "sha256:64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479", size = 174496, upload-time = "2026-07-06T21:32:40.467Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c8/6c2de1d55cf35ef8b92885d5ef280790f0fb9634d87ea1cc315176aecd61/cffi-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458", size = 185113, upload-time = "2026-07-06T21:32:41.761Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4e/e8d7cb5783f1841a3c8fb3a7735838d7484d08ec08c9f984b14cac1ac0e9/cffi-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d", size = 179927, upload-time = "2026-07-06T21:32:42.961Z" }, + { url = "https://files.pythonhosted.org/packages/1e/85/990925db5df586ec90beb97529c853497e7f85ba0234830447faf41c3057/cffi-2.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f", size = 184829, upload-time = "2026-07-06T21:32:44.324Z" }, + { url = "https://files.pythonhosted.org/packages/4b/92/e7bb136ad6b5352603732cf907ef862ca103f20f2031c1735a46300c20c9/cffi-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde", size = 184728, upload-time = "2026-07-06T21:32:45.683Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c0/d1ec30ffb370f748f2fb54425972bfef9871e0132e82fb589c46b6676049/cffi-2.1.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d", size = 214815, upload-time = "2026-07-06T21:32:48.557Z" }, + { url = "https://files.pythonhosted.org/packages/1b/dc/5620cf930688be01f2d673804291de757a934c90b946dbdc3d84130c2ea4/cffi-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7", size = 222429, upload-time = "2026-07-06T21:32:49.848Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a4/77b53abbf7a1e0beb9637edbef2a94d15f9c822f591e85d439ffd91519a6/cffi-2.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b", size = 210315, upload-time = "2026-07-06T21:32:51.221Z" }, + { url = "https://files.pythonhosted.org/packages/58/0c/f528df19cc94b675087324d4760d9e6d5bfae97d6217aa4fac43de4f5fcc/cffi-2.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7", size = 208859, upload-time = "2026-07-06T21:32:52.512Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/c9522a81c32132799a1972c39f5c5f8b4c8b9f00488a23feaa6c06f07741/cffi-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66", size = 221844, upload-time = "2026-07-06T21:32:53.704Z" }, + { url = "https://files.pythonhosted.org/packages/6e/28/bd53988b9833e8f8ad539d26f4c07a6b3f6bcb1e9e02e7ca038250b3428d/cffi-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe", size = 225287, upload-time = "2026-07-06T21:32:54.907Z" }, + { url = "https://files.pythonhosted.org/packages/79/99/0d0fd37f055224085f42bbb2c022d002e17dde4a97972822327b07d84101/cffi-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b", size = 223681, upload-time = "2026-07-06T21:32:56.329Z" }, + { url = "https://files.pythonhosted.org/packages/b0/80/c138990aa2a70b1a269f6e06348729836d733d6f970867943f61d367f8cc/cffi-2.1.0-cp312-cp312-win32.whl", hash = "sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a", size = 175269, upload-time = "2026-07-06T21:32:57.777Z" }, + { url = "https://files.pythonhosted.org/packages/a8/eb/f636456ff21a83fc13c032b58cc5dde061691546ac79efa284b2989b7982/cffi-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384", size = 185881, upload-time = "2026-07-06T21:32:59.253Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/400ea43e721727dca8a65c4521390e9196757caba4a45643acb2b63271b8/cffi-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6", size = 180088, upload-time = "2026-07-06T21:33:02.278Z" }, + { url = "https://files.pythonhosted.org/packages/96/88/a996879e2eeccb815f6e3a5967b12a308257412acec882039d386bd2aa7b/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda", size = 194331, upload-time = "2026-07-06T21:33:03.697Z" }, + { url = "https://files.pythonhosted.org/packages/58/85/7ae00d5c8dd6266f4e944c3db630f3c5c9a98b61d469c714d848b1d8138a/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b", size = 196966, upload-time = "2026-07-06T21:33:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e9/45c3a76ad8d43ad9261f4c95436da61128d3ca545d72b9612c0ab5be0b1c/cffi-2.1.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a", size = 184795, upload-time = "2026-07-06T21:33:06.699Z" }, + { url = "https://files.pythonhosted.org/packages/84/4c/82f132cb4418ee6d953d982b19191e87e2a6372c8a4ce36e50b69d6ade4a/cffi-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea", size = 184746, upload-time = "2026-07-06T21:33:08.071Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1c/4ed5a0e5bdca6cbc275556de3328dd1b76fd0c11cc13c88fe66d1d8715f2/cffi-2.1.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db", size = 214747, upload-time = "2026-07-06T21:33:09.671Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a6/e879bb68cc23a2bc9ba8f4b7d8019f0c2694bad2ab6c4a3701d429439f58/cffi-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f", size = 222392, upload-time = "2026-07-06T21:33:10.896Z" }, + { url = "https://files.pythonhosted.org/packages/88/f6/01890cfd63c08f8eb96a8319b0443690197d240a8bd6346048cf7bde9190/cffi-2.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d", size = 210285, upload-time = "2026-07-06T21:33:12.251Z" }, + { url = "https://files.pythonhosted.org/packages/a6/cf/2b684132056f438567b61e19d690dd31cd0921ace051e0a458be6074369e/cffi-2.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0", size = 208801, upload-time = "2026-07-06T21:33:13.617Z" }, + { url = "https://files.pythonhosted.org/packages/6f/08/f2e7d62c460faae0926f2d6e423694aa409ced3bc1fe2927a0a6e5f05416/cffi-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224", size = 221808, upload-time = "2026-07-06T21:33:15.466Z" }, + { url = "https://files.pythonhosted.org/packages/38/37/04f54b8e63a02f3d908332c9effbf8c366167c6f733ed8a3d4f79b7e2a1e/cffi-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c", size = 225241, upload-time = "2026-07-06T21:33:16.869Z" }, + { url = "https://files.pythonhosted.org/packages/a9/d6/c72eecca433cd3e681c65ed313ab4835d9d4a379704d0f628a6a05f51c2e/cffi-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a", size = 223588, upload-time = "2026-07-06T21:33:18.239Z" }, + { url = "https://files.pythonhosted.org/packages/c6/4b/e706f67279140f92939da3475ad610df18bfd52d50f14953a8e5fede71d5/cffi-2.1.0-cp313-cp313-win32.whl", hash = "sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2", size = 175248, upload-time = "2026-07-06T21:33:19.799Z" }, + { url = "https://files.pythonhosted.org/packages/5a/47/59eb7975cb0e4ef0afa764ea945b29a5bb4537a9f771cb7d6c8a5dd74c95/cffi-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512", size = 185717, upload-time = "2026-07-06T21:33:21.47Z" }, + { url = "https://files.pythonhosted.org/packages/5a/af/34fee85c48f8d94efc8597bc09470c9dd274c145f1c12e0fbc6ab6d38d74/cffi-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f", size = 180114, upload-time = "2026-07-06T21:33:22.515Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f0/81478e482afa03f6d18dc8f2afb5edc45b3080853b634b5ed91961be0998/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a", size = 194142, upload-time = "2026-07-06T21:33:23.657Z" }, + { url = "https://files.pythonhosted.org/packages/7d/95/8de304305cd9204974b0ca051b86d307cafca13aa575a0ef1b44d92c0d8c/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3", size = 196819, upload-time = "2026-07-06T21:33:25.007Z" }, + { url = "https://files.pythonhosted.org/packages/20/71/7c8372d30e42415602ed9f268f7cfd66f1b855fed881ecd168bcb45dbc0b/cffi-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d", size = 184965, upload-time = "2026-07-06T21:33:26.605Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5c/584e626835f0375c928176c04137c96927165cb8733cdb3150ec04e5ee5e/cffi-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac", size = 184952, upload-time = "2026-07-06T21:33:27.823Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d2/065fcae1c73979fac8e054462478d0ff8a29c40cdc2ed7ea5676a061df53/cffi-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6", size = 222353, upload-time = "2026-07-06T21:33:29.178Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a5/e8bbb1ce5b3ac2f53ad6a10bde44318a5a8d99d4f4a000d44a6e39aeb3e4/cffi-2.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913", size = 210051, upload-time = "2026-07-06T21:33:30.534Z" }, + { url = "https://files.pythonhosted.org/packages/28/ed/c127d3ac36e899c965e3361357c3befacd6578c03f40125183e41c3b219e/cffi-2.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d", size = 208630, upload-time = "2026-07-06T21:33:31.753Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d7/97d3136f81db489ec8d1d67748c110d6c994268fd7528014aa9f2b085e4e/cffi-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5", size = 221593, upload-time = "2026-07-06T21:33:33.044Z" }, + { url = "https://files.pythonhosted.org/packages/d3/27/93195977168ee63aed233a1a0993a2178798654d1f4bddcdd321d6fd3b21/cffi-2.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce", size = 225146, upload-time = "2026-07-06T21:33:34.224Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c1/6dbd291ee2ae5a50a034aa057207081f545923bbf15dad4511e985aafff5/cffi-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326", size = 223240, upload-time = "2026-07-06T21:33:35.57Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6f/ade5ce9863a57992a6ea3d0d10d7e29b8749fc127204b3d493d667b2815f/cffi-2.1.0-cp314-cp314-win32.whl", hash = "sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd", size = 177723, upload-time = "2026-07-06T21:33:51.626Z" }, + { url = "https://files.pythonhosted.org/packages/41/de/92b9eeed4ae4a21d6fd9b2a2c8505cbed573299902ea73981cc13f7ff62c/cffi-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb", size = 187937, upload-time = "2026-07-06T21:33:53.403Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1a/cc6ae6c2913a03aab8898eee57963cf1035b8df5872ed8b9115fcc7e2be8/cffi-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804", size = 183001, upload-time = "2026-07-06T21:33:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/14/f0/134c00ce0779ec86dea2aa1aac69339c2741a8045072676763512363a2ea/cffi-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714", size = 188538, upload-time = "2026-07-06T21:33:36.792Z" }, + { url = "https://files.pythonhosted.org/packages/50/d8/3b86aba791cb610d24e8a3e1b2cd529e71fa15096b04e4d4e360049d4a4c/cffi-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376", size = 188230, upload-time = "2026-07-06T21:33:38.011Z" }, + { url = "https://files.pythonhosted.org/packages/14/d0/117dcd9209255ad8571fbc8c92ef32593a1d294dcec91ddc4e4db50606f2/cffi-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98", size = 223899, upload-time = "2026-07-06T21:33:39.514Z" }, + { url = "https://files.pythonhosted.org/packages/b6/3d/f20f8b886b254e3ad10e15cd4186d3aed49f3e6a35ab37aab9f8f25f7c03/cffi-2.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13", size = 211652, upload-time = "2026-07-06T21:33:40.851Z" }, + { url = "https://files.pythonhosted.org/packages/28/3b/fad54de07260b93ddeef4b96d0131d57ea900675df1d410ae1deee52d7a6/cffi-2.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d", size = 210755, upload-time = "2026-07-06T21:33:42.183Z" }, + { url = "https://files.pythonhosted.org/packages/cc/82/3d5c705acb7abbba9bbd7d79b8e62e0f25b6120eb7ae6ac49f1b721722fe/cffi-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056", size = 223933, upload-time = "2026-07-06T21:33:43.603Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d0/47e338384ab6b1004241002fa616301020cea4fc95f283506565d252f276/cffi-2.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4", size = 226749, upload-time = "2026-07-06T21:33:45.046Z" }, + { url = "https://files.pythonhosted.org/packages/70/25/65bd5b58ea4bfdfc15cde02cb5365f89ef8ab8b2adfb8fe5c4bd4233382f/cffi-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94", size = 225703, upload-time = "2026-07-06T21:33:46.374Z" }, + { url = "https://files.pythonhosted.org/packages/dc/78/aa01ac599a8a4322533d45a1f9bc93b338276d2d59dabbe7c6d92a775c81/cffi-2.1.0-cp314-cp314t-win32.whl", hash = "sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76", size = 182857, upload-time = "2026-07-06T21:33:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/b9/26/d00496b22de4d4228f32dde94ad996f350c8aad676d63bcca0743c8dea4d/cffi-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5", size = 194065, upload-time = "2026-07-06T21:33:48.953Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dd/0c7dbf815a579ff005008a2d815a55d6bb047c349eef536d9dc53d3f0a8d/cffi-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8", size = 186404, upload-time = "2026-07-06T21:33:50.309Z" }, + { url = "https://files.pythonhosted.org/packages/55/c7/8c8c50cb11c6750051daf12164098a9a6f027ac4356967fd4d800a07f242/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c", size = 194121, upload-time = "2026-07-06T21:33:56.109Z" }, + { url = "https://files.pythonhosted.org/packages/99/e2/67680bf19a6b60d2bb7ff83baefa2a4c3d2d7dc0f3277034b802e1fc504c/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001", size = 196820, upload-time = "2026-07-06T21:33:57.288Z" }, + { url = "https://files.pythonhosted.org/packages/ed/da/4bbe583a3b3a5c8c60892124fe17f3fa3656523faf0d3484eae90f091853/cffi-2.1.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3", size = 184936, upload-time = "2026-07-06T21:33:58.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/4b/1f4c36ab273980d7aa75bb126ea4f8971f24a96108acad3a0a084028c57b/cffi-2.1.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc", size = 185045, upload-time = "2026-07-06T21:34:00.085Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c3/ad299dc38f3583f8d916b299f028af418a9ec98bc695fcbebeae7420691c/cffi-2.1.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699", size = 222342, upload-time = "2026-07-06T21:34:01.814Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d8/df4543cc087245044ed02ef3ad8e0a26619d0075ac7a77a12dc81177851b/cffi-2.1.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022", size = 210073, upload-time = "2026-07-06T21:34:03.255Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/fac738d73728c6cea2a88a2883dca54892496cbba88a1dc1f2909cb8a6f5/cffi-2.1.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0", size = 208551, upload-time = "2026-07-06T21:34:04.433Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3f/0b04a700dd64f465c93020253a793a82c9b4dff9961f48facd0df945d9b8/cffi-2.1.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1", size = 221649, upload-time = "2026-07-06T21:34:06.157Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/b7379a5704c79eda57ce075869ba70a0368d1c850f803b3c0d078d39dcaf/cffi-2.1.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28", size = 225203, upload-time = "2026-07-06T21:34:07.489Z" }, + { url = "https://files.pythonhosted.org/packages/5a/02/d5e6c43ea85c41bda2a184a3418f195fe7cf602967a8d2b94e085b83deef/cffi-2.1.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629", size = 223263, upload-time = "2026-07-06T21:34:08.712Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d8/772b8259bf75749adffb1c546828978381fb516f60cf701f6c83daf60c85/cffi-2.1.0-cp315-cp315-win32.whl", hash = "sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6", size = 177696, upload-time = "2026-07-06T21:34:26.355Z" }, + { url = "https://files.pythonhosted.org/packages/2f/dd/afa2191fc6d57fedd26e5844a2fe2fcc0bbfa00961bbaa5a41e4921e7cca/cffi-2.1.0-cp315-cp315-win_amd64.whl", hash = "sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853", size = 187914, upload-time = "2026-07-06T21:34:27.58Z" }, + { url = "https://files.pythonhosted.org/packages/05/ef/6cd4f8c671517162379dc79cfae5aea9106bc38abb89628d5c16adf6a838/cffi-2.1.0-cp315-cp315-win_arm64.whl", hash = "sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda", size = 183004, upload-time = "2026-07-06T21:34:28.905Z" }, + { url = "https://files.pythonhosted.org/packages/11/b6/12fc55092817a5faa26fb8c40c7f9d662e11a46ee248c137aafc42517d92/cffi-2.1.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc", size = 188378, upload-time = "2026-07-06T21:34:09.926Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2e/cdac88979f295fde5daa69622c7d2111e56e7ceb94f211357fbe452339e4/cffi-2.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca", size = 188319, upload-time = "2026-07-06T21:34:11.101Z" }, + { url = "https://files.pythonhosted.org/packages/e0/27/1d0b408497e41a74795af122d7b603c418c5fed0171450f899afd04e594f/cffi-2.1.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d", size = 223904, upload-time = "2026-07-06T21:34:12.606Z" }, + { url = "https://files.pythonhosted.org/packages/8b/31/e115c985105dd7ffb32444505f18ceb874bb42d992af05d5dced7ecf1980/cffi-2.1.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8", size = 211554, upload-time = "2026-07-06T21:34:13.987Z" }, + { url = "https://files.pythonhosted.org/packages/5a/67/9e6e09409336d9e515c58367e7cfcf4f89df06ad25252675595a58eb59d5/cffi-2.1.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd", size = 210795, upload-time = "2026-07-06T21:34:15.972Z" }, + { url = "https://files.pythonhosted.org/packages/19/e5/d3cc82a4a0be7902af279c04181ad038449c096734464a5ae1de3e1401bd/cffi-2.1.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f", size = 223843, upload-time = "2026-07-06T21:34:17.509Z" }, + { url = "https://files.pythonhosted.org/packages/b9/65/b434abc97ce7cecc2c640fde160507c0ecc7e21544b483ba3325d2e2ea17/cffi-2.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc", size = 226773, upload-time = "2026-07-06T21:34:19.05Z" }, + { url = "https://files.pythonhosted.org/packages/b5/9f/d4dc66ca651eb1145a133314cda721abf13cfac3d28c4a0402263ae6ad75/cffi-2.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9", size = 225719, upload-time = "2026-07-06T21:34:20.576Z" }, + { url = "https://files.pythonhosted.org/packages/68/5a/e536c528bc8057496c360c0978559a2dc45653f89dd6151078aa7d8fca1a/cffi-2.1.0-cp315-cp315t-win32.whl", hash = "sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b", size = 182760, upload-time = "2026-07-06T21:34:22.059Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0b/0ffe8b82d3875bced5fa1e7986a7a46b748262a40ab7f60b475eb9fb1bb3/cffi-2.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5", size = 193769, upload-time = "2026-07-06T21:34:23.589Z" }, + { url = "https://files.pythonhosted.org/packages/a0/17/1073b53b68c9b5ca6914adf5f8bf55aacc2d3be102418c90700160ea8605/cffi-2.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210", size = 186405, upload-time = "2026-07-06T21:34:24.857Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5", size = 317075, upload-time = "2026-07-07T14:32:56.021Z" }, + { url = "https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2", size = 213837, upload-time = "2026-07-07T14:32:57.78Z" }, + { url = "https://files.pythonhosted.org/packages/b3/46/03ddc7da576d814fe0a36dd1f0fd3258e95404b4b2e3c026b7923d7e133f/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a", size = 235503, upload-time = "2026-07-07T14:32:59.205Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6e/de0229a7ef40f6f9d28a837eebf4ec47bdca5dab4e900c84f22919af636a/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29", size = 229944, upload-time = "2026-07-07T14:33:00.803Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c", size = 221276, upload-time = "2026-07-07T14:33:02.199Z" }, + { url = "https://files.pythonhosted.org/packages/44/95/80282cce0fae9c3061203d723ee87da996aed79679e65d8935050ee7ca1f/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b", size = 205260, upload-time = "2026-07-07T14:33:03.698Z" }, + { url = "https://files.pythonhosted.org/packages/0c/74/2f62c8821b969ea3bd67cc2e6976834f48ca5d12664d2559ebcd9bcfbed7/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db", size = 217786, upload-time = "2026-07-07T14:33:05.12Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8d/feabb82cb49fcad14515b1d7d1ca4787b0da7fc723a212bf89bc9e0fac52/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993", size = 216798, upload-time = "2026-07-07T14:33:06.629Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ff/c946d63bc3786d5b84d960b0f7ab7e25b828486a946b5aa997625bcaf6a6/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da", size = 206429, upload-time = "2026-07-07T14:33:08.006Z" }, + { url = "https://files.pythonhosted.org/packages/af/ba/5e5007c370702f85d2ef75791fac7943ed41e080364a673b20142e430e3e/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3", size = 223066, upload-time = "2026-07-07T14:33:09.783Z" }, + { url = "https://files.pythonhosted.org/packages/83/d5/9096aa3cf532dfad237861544eb47a0f20d5adbf1039760fed8eaae935d9/charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d", size = 150456, upload-time = "2026-07-07T14:33:11.217Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1", size = 161410, upload-time = "2026-07-07T14:33:12.743Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8d/1569f4d0032d6ba2a4fe4591c35bf87868c600c41a71eb5c2e1ffa8464c2/charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec", size = 152649, upload-time = "2026-07-07T14:33:14.173Z" }, + { url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" }, + { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" }, + { url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" }, + { url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" }, + { url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" }, + { url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" }, + { url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" }, + { url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" }, + { url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" }, + { url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" }, + { url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" }, + { url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" }, + { url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" }, + { url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" }, + { url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" }, + { url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" }, + { url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" }, + { url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" }, + { url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" }, + { url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" }, + { url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" }, + { url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" }, + { url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" }, + { url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" }, + { url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" }, + { url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" }, + { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, +] + +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + +[[package]] +name = "cloudpickle" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +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 = "cuda-bindings" +version = "13.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/6b/457ca12dad3ee9bfcc9a545cfd6b64b359ba49de40f776f6e028e678f262/cuda_bindings-13.3.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5879712accf6e14bb01aa5e67440eb84998b8d104b509cc7a6dc0b8f656a474", size = 6053539, upload-time = "2026-05-29T23:11:43.19Z" }, + { url = "https://files.pythonhosted.org/packages/95/7a/c5e3c34a409b148f5c0f5a4ea374158f95d488862c1dffedf9aa5c639df9/cuda_bindings-13.3.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04436a9364059c84b8f9636f359eccda1cf814341f5b670c71d80d2f79dbc708", size = 6674166, upload-time = "2026-05-29T23:11:45.478Z" }, + { url = "https://files.pythonhosted.org/packages/ce/67/5e7dba1ba576dd73da5dee894ca076ca5e959450dfff66d6d510a255d1f7/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7855c4868aabc0cfae28abbe83d56734bdfbd08f08fc234ac1912a12858bf49", size = 6025351, upload-time = "2026-05-29T23:11:49.685Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/6d2e9047d1fb243dbaa364b01e0297534b9ed7fd27dba1c9f361519cf69b/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e32d08f71ebcdf00f0f41eab2eb37e8da94c8ed411cc9f7f7a019ce6b34abe3a", size = 6657965, upload-time = "2026-05-29T23:11:52.227Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6e/2394f8163360f8391f8f1b7e72d300a82724edb81a7b7084c799fbd4c91f/cuda_bindings-13.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9efb21c1ee64981e184b9e0ba5eb3179e5ba3d4b51665a6cb52b8ef3d01a7cbf", size = 5920504, upload-time = "2026-05-29T23:11:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/34/c2/ef9b6a63f7dc432712a462c816662e662e00d38caa9b861c8c2588195d03/cuda_bindings-13.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2732904099e0a4d4db774a5fc6d91ee95fae065b4d2ecabb4968c5fe2406c9d7", size = 6476660, upload-time = "2026-05-29T23:11:59.188Z" }, + { url = "https://files.pythonhosted.org/packages/b1/81/bff68ce829999c1e4209c761bbf903b1c06ec570416ddb25020864ad5907/cuda_bindings-13.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ab2f74ed65bfef4163ba07a8db16f1085e0729291db12a2423aff84ee8278b8", size = 6013639, upload-time = "2026-05-29T23:12:03.509Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e0/c8a1f0c8f9ffdea4f5fe6dbab89b326cef4d85caf489dad39e209da89416/cuda_bindings-13.3.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd4c814d311ec08c981f6dded1dbe7d4b371067ee4f6c14cccec4bde9590f80", size = 6534419, upload-time = "2026-05-29T23:12:05.633Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/83b1f563925b290f2d11a01a77a84013ba56052fe3653a5bef3ccfbb43d6/cuda_bindings-13.3.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3c772dfff49681541d59630c90f858e173ac926b9c593a2b7123f2a1043cc76", size = 5809771, upload-time = "2026-05-29T23:12:10.422Z" }, + { url = "https://files.pythonhosted.org/packages/12/20/e79b4bfe98f075195afb6343d41c498f9dbd2d161d7021d4d28bceb83581/cuda_bindings-13.3.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36febb7c1079d68a981dbbd8d5a67235b399802b82075c9388624719607e52b9", size = 6358584, upload-time = "2026-05-29T23:12:12.767Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.5.6" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/53/8fc9b0cdc5b7f62746e6a01b85b6461e5ae27f871010a5fcf8fa6950766d/cuda_pathfinder-1.5.6-py3-none-any.whl", hash = "sha256:7e4c07c117b78ba1fb35dac4c444d21f3677b1b1ff56175c53a8e3025c5b43c0", size = 52972, upload-time = "2026-06-30T00:58:04.34Z" }, +] + +[[package]] +name = "cuda-toolkit" +version = "13.0.3.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/c7/a79086a62c98befcdb8349656c6f114e2db3b8b2422f6e25c97a7f2a9a3c/cuda_toolkit-13.0.3.0-py2.py3-none-any.whl", hash = "sha256:d693caaa261214ddd7dbb60d68e71cbed884e68c2be7509778f3051da0b91c3f", size = 2512, upload-time = "2026-04-14T00:50:08.173Z" }, +] + +[package.optional-dependencies] +cublas = [ + { name = "nvidia-cublas", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cuda-nvrtc", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, +] +cudart = [ + { name = "nvidia-cuda-runtime", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, +] +cufft = [ + { name = "nvidia-cufft", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, +] +cufile = [ + { name = "nvidia-cufile", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, +] +cupti = [ + { name = "nvidia-cuda-cupti", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, +] +curand = [ + { name = "nvidia-curand", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, +] +cusolver = [ + { name = "nvidia-cublas", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cusolver", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cusparse", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, +] +cusparse = [ + { name = "nvidia-cusparse", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, +] +nvjitlink = [ + { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, +] +nvrtc = [ + { name = "nvidia-cuda-nvrtc", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, +] +nvtx = [ + { name = "nvidia-nvtx", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, +] + +[[package]] +name = "decorator" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/60/8b/32f9823da46cde7df2087faa08cd98d01b908f8dcab982cdba9c84e85355/decorator-5.3.1.tar.gz", hash = "sha256:4cbcdd55a6efadb9dbea26b858f4fb3264567b52d69ca0d25b721b553f60ea82", size = 58084, upload-time = "2026-05-18T06:03:28.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl", hash = "sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c", size = 10365, upload-time = "2026-05-18T06:03:26.517Z" }, +] + +[[package]] +name = "demucs" +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dora-search" }, + { name = "einops" }, + { name = "julius" }, + { name = "lameenc" }, + { name = "openunmix" }, + { name = "pyyaml" }, + { name = "torch" }, + { name = "torchaudio" }, + { name = "tqdm" }, +] +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 = "dora-search" +version = "0.1.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "omegaconf" }, + { name = "retrying" }, + { name = "submitit" }, + { name = "torch" }, + { name = "treetable" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d5/9d/9a13947db237375486c0690f4741dd2b7e1eee20e0ffcb55dbd1b21cc600/dora_search-0.1.12.tar.gz", hash = "sha256:2956fd2c4c7e4b9a4830e83f0d4cf961be45cfba1a2f0570281e91d15ac516fb", size = 87111, upload-time = "2023-05-23T14:36:24.743Z" } + +[[package]] +name = "einops" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/77/850bef8d72ffb9219f0b1aac23fbc1bf7d038ee6ea666f331fa273031aa2/einops-0.8.2.tar.gz", hash = "sha256:609da665570e5e265e27283aab09e7f279ade90c4f01bcfca111f3d3e13f2827", size = 56261, upload-time = "2026-01-26T04:13:17.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/09/f8d8f8f31e4483c10a906437b4ce31bdf3d6d417b73fe33f1a8b59e34228/einops-0.8.2-py3-none-any.whl", hash = "sha256:54058201ac7087911181bfec4af6091bb59380360f069276601256a76af08193", size = 65638, upload-time = "2026-01-26T04:13:18.546Z" }, +] + +[[package]] +name = "fastapi" +version = "0.115.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236, upload-time = "2025-03-23T22:55:43.822Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164, upload-time = "2025-03-23T22:55:42.101Z" }, +] + +[[package]] +name = "filelock" +version = "3.32.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/80/8232b582c4b318b817cf1274ba74976b07b34d35ef439b3eb948f98645a1/filelock-3.32.0.tar.gz", hash = "sha256:7be2ad23a14607ccc71808e68fe30848aeace7058ace17852f68e2a68e310402", size = 213757, upload-time = "2026-07-21T13:17:42.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/79/b4c714bef36bc4ec2beeae1e0c124f0223888cd8c6feb1cdc56038116920/filelock-3.32.0-py3-none-any.whl", hash = "sha256:d396bea984af47333ef05e50eae7eff88c84256de6112aea0ec48a233c064fe3", size = 97732, upload-time = "2026-07-21T13:17:41.55Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/10/a1/ae4e3e5003468d6391d2c77b6fa1cd73bd5d13511d81c642d7b28ac90ed4/fsspec-2026.6.0.tar.gz", hash = "sha256:f5bac145310fe30e16e1471bd6840b2d990d609e872251d7e674241822abf01a", size = 313646, upload-time = "2026-06-16T01:57:28.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl", hash = "sha256:02e0b71817df9b2169dc30a16832045764def1191b43dcff5bb85bdee212d2a1", size = 203949, upload-time = "2026-06-16T01:57:26.358Z" }, +] + +[[package]] +name = "greenlet" +version = "3.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/f1/fbbfef6af0bad0548f09bc28948ea3c275b4edb19e17fc5ca9900a6a634d/greenlet-3.5.3.tar.gz", hash = "sha256:a61efc018fd3eb317eeca31aba90ee9e7f26f22884a79b6c6ec715bf71bb62f1", size = 200270, upload-time = "2026-06-26T19:28:24.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/58/5404031044f55afad7aad1aff8be3f22b1bed03e237cfeabbc7e5c8cfde0/greenlet-3.5.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:aca9b4ce85b152b5524ef7d88170efdff80dc0032aa8b75f9aaf7f3479ea95b4", size = 287424, upload-time = "2026-06-26T18:20:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bf/1c65e9b94a54d547068fa5b5a8a06f221f3316b48908e08668d29c77cb50/greenlet-3.5.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f71be4920368fe1fabeeaa53d1e3548337e2b223d9565f8ad5e392a75ba23fc", size = 606523, upload-time = "2026-06-26T19:07:08.859Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c7/b66baacc95775ad511287acb0137b95574a9ce5491902372b7564799d790/greenlet-3.5.3-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d77e67f65f98449e3fb83f795b5d0a8437aead2f874ca89c96576caf4be3af6", size = 618315, upload-time = "2026-06-26T19:10:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/78/2b/28ed29463522fdbe4c15b1f63922041626a7478316b34ab4adda3f0a4aba/greenlet-3.5.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8540f1e6205bd13ca0ce685581037219ca54a1b41a0a15d228c6c9b8ad5903d7", size = 617381, upload-time = "2026-06-26T18:32:16.077Z" }, + { url = "https://files.pythonhosted.org/packages/2a/7b/ad04e9d1337fc04965dc9fc616b6a72cb65a24b800a014c011ec812f5489/greenlet-3.5.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ef56fe650f50575bf843acde967b9c567687f3c22340941a899b7bc56e956a8", size = 1577771, upload-time = "2026-06-26T19:09:01.537Z" }, + { url = "https://files.pythonhosted.org/packages/d8/33/6c87ab7ba663f70ca21f3022aad1ffe56d3f3e0521e836c2415e13abcc3c/greenlet-3.5.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5121af01cf911e70056c00d4b46d5e9b5d1415550038573d744138bacb59e6b8", size = 1644048, upload-time = "2026-06-26T18:31:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/1c/35/f0d8ee998b422cf8693b270f098e55d8d4ec8006b061b333f54f177d28d9/greenlet-3.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:0f41e4a05a3c0cb31b17023eff28dd111e1d16bf7d7d00406cd7df23f31398a7", size = 239137, upload-time = "2026-06-26T18:23:21.664Z" }, + { url = "https://files.pythonhosted.org/packages/fb/96/b9820295576ef18c9edc404f10e260ae7215ceaf3781a54b720ed2627862/greenlet-3.5.3-cp311-cp311-win_arm64.whl", hash = "sha256:ec6f1af59f6b5f3fc9678e2ea062d8377d22ac644f7844cb7a292910cf12ff44", size = 237630, upload-time = "2026-06-26T18:24:00.281Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6e/4c37d51a2b7f82d2ff11bb6b5f7d766d9a011726624af255e843727627a3/greenlet-3.5.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:719757059f5a53fd0dde23f78cffeafcdd97b21c850ddb7ca684a3c1a1f122e2", size = 288685, upload-time = "2026-06-26T18:22:08.977Z" }, + { url = "https://files.pythonhosted.org/packages/7a/73/815dd90131c1b71ebdf53dbc7c276cafec2a1173b97559f97aba72724a87/greenlet-3.5.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efa9f765dd09f9d0cdac651ffdf631ee59ec5dc6ee7a73e0c012ba9c52fbdf5b", size = 604761, upload-time = "2026-06-26T19:07:10.114Z" }, + { url = "https://files.pythonhosted.org/packages/9f/57/079cfe76bcef36b153b25607ee91c6fcb58f17f8b23c86bbbeabe0c88d72/greenlet-3.5.3-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7faba15ac005376e02a0384504e0243be3370ce010296a44a820feb342b505ab", size = 617044, upload-time = "2026-06-26T19:10:07.25Z" }, + { url = "https://files.pythonhosted.org/packages/37/87/b4d095775a3fb1bcafbb483fc206b27ebb785724c83051447737085dc54e/greenlet-3.5.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:87142215824be6ac05e2e8e2786eec307ccbc27c36723c3881959df654af6861", size = 614244, upload-time = "2026-06-26T18:32:17.594Z" }, + { url = "https://files.pythonhosted.org/packages/8a/70/7559b609683650fa2b95b8ab84b4ab0b26556a635d19675e12aa832d826d/greenlet-3.5.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215275b1b49320987352e6c1b054acca0064f965a2c66992bed9a6f7d913f149", size = 1574210, upload-time = "2026-06-26T19:09:03.077Z" }, + { url = "https://files.pythonhosted.org/packages/ae/73/be55392074c60fc37655ca40fa6022457bfbf6718e9e342a7b0b41f96dd2/greenlet-3.5.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6b1b0eed82364b0e32c4ea0f221452d33e6bb17ae094d9f72aed9851812747ea", size = 1638627, upload-time = "2026-06-26T18:31:44.748Z" }, + { url = "https://files.pythonhosted.org/packages/14/40/c57489acf8e37d74e2913d4eff63aa0dba17acccc4bdeef874dde2dbbec9/greenlet-3.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:cde8adafa2365676f74a979744629589999093bc86e2484214f58e61df08902c", size = 239882, upload-time = "2026-06-26T18:23:27.518Z" }, + { url = "https://files.pythonhosted.org/packages/71/fd/6fea0e3d6600f785069481ee637e09378dd4118acdfd38ad88ae2db31c98/greenlet-3.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:c4e7b79d83805475f0102008843f6eb45fd3bb0b2e88c774adab5fbaab27117d", size = 238211, upload-time = "2026-06-26T18:22:37.671Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ff/a620267401db30a50cc8450ee90730e2d4a85658c055c0e760d4ed47fb13/greenlet-3.5.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:c8d87c2134d871df96ecdea9cec7cbaab286dadab0f56476e57aaf9e8ac11550", size = 287609, upload-time = "2026-06-26T18:21:14.724Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fa/5401ac78021c826a25b6dde0c705e0a8f29b617509f9185a31dac15fbe1b/greenlet-3.5.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2d185dd1621757e70c3861cceffd5317ab4e7ed7eb09c82994828468527ade5", size = 607435, upload-time = "2026-06-26T19:07:11.412Z" }, + { url = "https://files.pythonhosted.org/packages/e9/76/1dc144a2e56e65d36405078ed774224375ea520a1870a6e46e08bb4ac7bf/greenlet-3.5.3-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1c514a468149bf8fbbab874188a3535cd8a48a3e353eb53a3d424296f8dbacd3", size = 619787, upload-time = "2026-06-26T19:10:08.396Z" }, + { url = "https://files.pythonhosted.org/packages/bf/87/c298cee62df1de4ad7fec32abda73526cff347fd143a6ed4ac369246668a/greenlet-3.5.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:915f887cf2682b66419b879423a2e072634aa7b7dce6f3ada4957cfced3f1e9a", size = 616786, upload-time = "2026-06-26T18:32:19.128Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2e/e6f009885ed0705ccf33fe0583c117cfd03cde77e31a596dd5785a30762b/greenlet-3.5.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:766cfd421c13e450feb340cd472a3ed9957d438727b7b4593ad7c76c5d2b0deb", size = 1574316, upload-time = "2026-06-26T19:09:04.273Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fe/43fd110b01e40da0adb7c90ac7ea744bef2d43dca00de5095fd2351c2a68/greenlet-3.5.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2ecda9ec22edf38fa389369eaed8c3d37c05f3c54e69f69438dbb2cc1de1458b", size = 1638614, upload-time = "2026-06-26T18:31:46.297Z" }, + { url = "https://files.pythonhosted.org/packages/0f/7c/062447147a61f8b4337b156fe70d32a165fcf2f89d7ca6255e572806705c/greenlet-3.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:c82304750f057167ff60d188df1d0cc1764ce9567eadf03e6a7443bcedd0b30b", size = 239850, upload-time = "2026-06-26T18:21:54.613Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7e/220a7f5824a64a60443fc03b39dfac4ea63a7fb6d481efa27eafa928e7f4/greenlet-3.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:dc133a1569ee667b2a6ef56ce551084aeefd87a5acbc4736d336d1e2edc6cfc4", size = 238141, upload-time = "2026-06-26T18:22:48.507Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/43e116ee114b28737ba7e12952a0d4e2f55944d0f84e42bc91ba7192a3c9/greenlet-3.5.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:fd2e02fa07485778536a036222d616ab957b1d533f36b3ed98ce725d9c9d3117", size = 288202, upload-time = "2026-06-26T18:23:49.604Z" }, + { url = "https://files.pythonhosted.org/packages/82/2f/146d218299046a43d1f029fd544b3d110d0f175a09c715c7e8da4a4a345d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df0a0628d1597eb0897b62f55d1343f772405fd25f3b2a796c76874b0c2e22e8", size = 654096, upload-time = "2026-06-26T19:07:12.71Z" }, + { url = "https://files.pythonhosted.org/packages/a0/cc/04738cafb3f45fa991ea44f9de94c47dcec964f5a972300988a6751f49d9/greenlet-3.5.3-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ebd933a6adabc298bab47731a130fe6bfb888bd934eee37810f151159544540d", size = 666304, upload-time = "2026-06-26T19:10:09.503Z" }, + { url = "https://files.pythonhosted.org/packages/ce/aa/4e0dad5e605c270c784ab911c43da6adb136ccd4d81180f763ca429a723d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b9d501b40e80b70e32323c799dd9b420a5577a9601469d362ae1ffb690f3a7c", size = 663635, upload-time = "2026-06-26T18:32:20.802Z" }, + { url = "https://files.pythonhosted.org/packages/d1/50/13efdbea246fe3d3b735e191fec08fb50809f53cd2383ebe123d0809e44b/greenlet-3.5.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a1fad1d11e7d6aab184107baa8e4ece11ccba3ec9599cd7efa5ff4d70d43256a", size = 1621252, upload-time = "2026-06-26T19:09:05.647Z" }, + { url = "https://files.pythonhosted.org/packages/f7/22/c0a336ae4a1410fd5f5121098e5bfbf1865f64c5ef80b4b5412886c4a332/greenlet-3.5.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fad5aec764399f1b5cc347ad250a59660f20c8f8888ea6bae1f93b769cce1154", size = 1684824, upload-time = "2026-06-26T18:31:47.738Z" }, + { url = "https://files.pythonhosted.org/packages/7a/94/91aec0030bea75c4b3244251d0de60a1f3432d1ecb53ab6c437fb5c3ba61/greenlet-3.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:7669aa24cf2a1041d6f7899575b494a3ab4cf68bfcc8609b1dc0be7272db835e", size = 240754, upload-time = "2026-06-26T18:22:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/e5/06/68d0983e79e02138f64b4d303c500c27ddb48e5e77f3debb80888a921eae/greenlet-3.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:5b4807c4082c9d1b6d9eed56fcd041863e37f2228106eef24c30ca096e238605", size = 239549, upload-time = "2026-06-26T18:22:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/91/95/3e161213d7f1d378d15aa9e792093e9bfe01844680d04b7fd6e0107c9098/greenlet-3.5.3-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:271a8ea7c1024e8a0d7dd2be66dd66dda8a07193f41a17b9e924f7600f5b62be", size = 296389, upload-time = "2026-06-26T18:22:20.657Z" }, + { url = "https://files.pythonhosted.org/packages/00/92/715c44721abe2b4d1ae9abde4179411868a5bff312479f54e105d372f131/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19131729ae0ddc3c2e1ef85e650169b5e37ee32e400f215f78b94d7b0d567310", size = 653382, upload-time = "2026-06-26T19:07:14.209Z" }, + { url = "https://files.pythonhosted.org/packages/a0/83/37a10372a1090a6624cca8e74c12df1a36c2dc36429ed0255b7fb1aeee23/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1540dd8e5fc2a5aec40fbb98ef8e149fa47c89a4b4a1cf2575a14d3d1869d7a8", size = 659401, upload-time = "2026-06-26T19:10:10.876Z" }, + { url = "https://files.pythonhosted.org/packages/db/e2/d1509cad4207da559cc42986ecdd8fc67ad0d1bba2bf03023c467fd5e0f3/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e81fa194a1d20967877bdf9c7794db2bc99063e5be36aee710c08f04c5bb087f", size = 656969, upload-time = "2026-06-26T18:32:22.272Z" }, + { url = "https://files.pythonhosted.org/packages/86/7d/eaf70de20aadca3a5884aec58362861c64ce45e7b277f47ed026926a3b89/greenlet-3.5.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:55cf4d777485d43110e47133cbba6d74a8885a87ec1227ef0267f9ee80c5aa21", size = 1617822, upload-time = "2026-06-26T19:09:06.893Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f9/414d38fc400ae4350d4185eaad1827676f7cf5287b9136e0ed1cbbe20a7f/greenlet-3.5.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:12a248ba75f6a9a236375f52296c498c89ff1d8badf32deb9eca7abd5853f7da", size = 1677983, upload-time = "2026-06-26T18:31:49.396Z" }, + { url = "https://files.pythonhosted.org/packages/e4/15/7edb977e08f9bff702fe42d6c902702786ff6b9694058b4e6a2a6ac90e57/greenlet-3.5.3-cp314-cp314t-win_amd64.whl", hash = "sha256:efc6bd60ea02e085862c74a3ef64b147ffc6f1a5ea7d9f26e7a939943f68c1e3", size = 243626, upload-time = "2026-06-26T18:24:41.485Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8a/93928dce91e6b3598b5e779e8d1fd6576a504640c58e78627077f6a7a91a/greenlet-3.5.3-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:ea03f2f04367845d6b58eeed276e1e56e51f0b97d8ad5a88a7d20a91dc9056cc", size = 288860, upload-time = "2026-06-26T18:22:48.07Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ca/69db42d447a1378043e2c8f19c09cbbd1263371505053c496b49066d3d16/greenlet-3.5.3-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78dbef602fda6d97d957eb7937f70c9ce9e9527330347f8f6b6f9e554a9e7a47", size = 659747, upload-time = "2026-06-26T19:07:15.565Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0b/af7ac2ef8dd41e3da1a40dda6305c23b9a03e13ba975ec916357b50f8575/greenlet-3.5.3-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f73857adb8fee13fa56c172bd11262f888c0c648f9fea113e777bb2c7904a81", size = 670419, upload-time = "2026-06-26T19:10:12.293Z" }, + { url = "https://files.pythonhosted.org/packages/51/1e/1d51640cacbfc455dbe9f9a9f594c49e4e244f63b9971a2f4764e46cc53d/greenlet-3.5.3-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:232fec92e823addaf02d9472cf7381e24a1d046a6ced1103c5caa4c21b9dfc1d", size = 668787, upload-time = "2026-06-26T18:32:24.298Z" }, + { url = "https://files.pythonhosted.org/packages/21/66/4030d5b0b5894500023f003bb054d9bb354dfbd1e186c3a296759172f5f5/greenlet-3.5.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:2421c3564da9429d5586d46ca31ebb26516b5498a802cf65c041a8e8a8980d34", size = 1626305, upload-time = "2026-06-26T19:09:08.281Z" }, + { url = "https://files.pythonhosted.org/packages/0e/50/5221371c7550108dfa3c378debc41d032aa9c78e89abb01d8011cfc93289/greenlet-3.5.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e0f0d160f0b2e558e6c75f7930967183255dc9735e5f5b8cae58ee09c9576d8b", size = 1688631, upload-time = "2026-06-26T18:31:51.278Z" }, + { url = "https://files.pythonhosted.org/packages/68/5d/00d469daae3c65d2bf620b10eee82eb022127d483c6bc8c69fae6f3fbf17/greenlet-3.5.3-cp315-cp315-win_amd64.whl", hash = "sha256:dd99329bbc15ca78dcc583dba05d0b1b0bae01ab6c2174989f5aaee3e41ac930", size = 241027, upload-time = "2026-06-26T18:22:38.203Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e8/883785b44c5780ed71e83d3e4437e710470be17a2e181e8b601e2da0dc4a/greenlet-3.5.3-cp315-cp315-win_arm64.whl", hash = "sha256:499fef2acede88c1864a57bb586b4bf533c81e1b82df7ab93451cdb47dfec227", size = 240085, upload-time = "2026-06-26T18:23:54.217Z" }, + { url = "https://files.pythonhosted.org/packages/1c/da/4f4a8450962fad137c1c8981a3f1b8919d06c829993d4d476f9c525d5173/greenlet-3.5.3-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:176bc16a721fa5fc294d70b87b4dfa5fbdd251b3da5d5372735ecef9bd7d6d0c", size = 297221, upload-time = "2026-06-26T18:23:27.176Z" }, + { url = "https://files.pythonhosted.org/packages/57/66/b3bfae3e220a9b63ea539a0eea681800c69ab1aada757eae8789f183e7ce/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:629b614d2b786e89c50440e246f33eea78f58a962d0bdbbcc809e6d13605903f", size = 657221, upload-time = "2026-06-26T19:07:16.973Z" }, + { url = "https://files.pythonhosted.org/packages/7b/81/b6d4d73a709684fc77e7fa034d7c2fe82cffa9fc920fadcaa659c2626213/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b2e857ae16f5f72142edf75f9f176fe7526ba19a2841df1420516f83831c9f2", size = 663226, upload-time = "2026-06-26T19:10:13.723Z" }, + { url = "https://files.pythonhosted.org/packages/f5/07/e210b02b589f16e74ff48b730690e4a34ffe984219fce4f3c1a0e7ec8545/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e515757e2e36bcbf1fad09a46e1557e8b1ae1797d4b44d09da7deed88ad28608", size = 660802, upload-time = "2026-06-26T18:32:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/eb/2e/5303eb3fa06bca089060f479707182a93e360683bc252acf846c3090d34e/greenlet-3.5.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b363d46ed1ea431825fdb01471bb024fc08399bad1572a616e853c7684415adb", size = 1622157, upload-time = "2026-06-26T19:09:09.527Z" }, + { url = "https://files.pythonhosted.org/packages/54/70/50de47a488f14df260b50ae34fb5d56016e308b098eab02c878b5223c26a/greenlet-3.5.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:e44da2f5bbdaabaf7d80b73dbb430c7035771e9f244e3c8b769715c9d8fa0a16", size = 1681159, upload-time = "2026-06-26T18:31:52.986Z" }, + { url = "https://files.pythonhosted.org/packages/a7/13/1055e1dda7882073eda533e2b96c62e55bbd2db7fda6d5ece992febc7071/greenlet-3.5.3-cp315-cp315t-win_amd64.whl", hash = "sha256:8ff8bed3e3baa20a3ea261ce00526f1898ad4801d4886fd2220580ee0ad8fadf", size = 244007, upload-time = "2026-06-26T18:22:04.353Z" }, + { url = "https://files.pythonhosted.org/packages/b4/0d/ca7d15afbdc397e3401134c9e1800d51d12b829661786187a4ad08fe484f/greenlet-3.5.3-cp315-cp315t-win_arm64.whl", hash = "sha256:b7068bd09f761f3f5b4d214c2bed063186b2a86148c740b3873e3f56d79bac31", size = 242586, upload-time = "2026-06-26T18:23:37.93Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +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 = "hatchling" +version = "1.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "pathspec" }, + { name = "pluggy" }, + { name = "trove-classifiers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/8a/cc1debe3514da292094f1c3a700e4ca25442489731ef7c0814358816bb03/hatchling-1.27.0.tar.gz", hash = "sha256:971c296d9819abb3811112fc52c7a9751c8d381898f36533bb16f9791e941fd6", size = 54983, upload-time = "2024-12-15T17:08:11.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/e7/ae38d7a6dfba0533684e0b2136817d667588ae3ec984c1a4e5df5eb88482/hatchling-1.27.0-py3-none-any.whl", hash = "sha256:d3a2f3567c4f926ea39849cdf924c7e99e6686c9c8e288ae1037c8fa2a5d937b", size = 75794, upload-time = "2024-12-15T17:08:10.364Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httptools" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/e5/d471fcb0e14523fe1c3f4ba58ca52480e7bd70ad7109a3846bc75892f7fb/httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999", size = 271342, upload-time = "2026-05-25T22:17:48.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/d2/c3eedaef57de65c3cc5f8dc244cf12d09c84ad258a479055aad6db23206c/httptools-0.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ed377e64805bdba4943c82717333f8f8603a13b09aff9cead2717c6c817fb168", size = 208428, upload-time = "2026-05-25T22:16:59.717Z" }, + { url = "https://files.pythonhosted.org/packages/f1/94/dfe435d90d0ef61ec0f2cc3d480eef78c59727c6c2ce039f433882f6131a/httptools-0.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9518c406d7b310f05adb1a37f80acabac40504a575d7c0da6d3e365c695ac20d", size = 113366, upload-time = "2026-05-25T22:17:00.795Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d4/13025f1a56e615dcb331e0bbe2d9a1143212b58c263385fc5d2e558f5bac/httptools-0.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:57278e6fa0424c42a8a3e454828ab4f0aff27b40cddf9679579b98c6dce6a376", size = 464676, upload-time = "2026-05-25T22:17:02.014Z" }, + { url = "https://files.pythonhosted.org/packages/bf/95/4c1c26c0b985f8a3331682d802598f14e32dc41bf7509266eb2c04ad4801/httptools-0.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbb8caadb2b742d293169d2b458b5c001ef70e3158704aa3d3ef9597624c5d1d", size = 464235, upload-time = "2026-05-25T22:17:03.109Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/6735be2b0ca527718c431cdb8e5f70c3862c0844a687df0f572c51e11497/httptools-0.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:52dd695b865fe96d9d2b16b64a895f3f57bf3cb064e8383cd3b5713a069e8085", size = 449809, upload-time = "2026-05-25T22:17:04.443Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f9/5811c74f37a758c8a4aa3dc430375119d335947e883efc4664d8f3559a41/httptools-0.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:20b4aac66ff65f7db06a375808b78f42a94970aa22e826b3cb2b43eb09174124", size = 452174, upload-time = "2026-05-25T22:17:05.476Z" }, + { url = "https://files.pythonhosted.org/packages/cc/94/97b75870dea07b71e3ec535cebe525b08d723152e4c7d13fa887e51f4de2/httptools-0.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1b4c8e7a489a0d750d91894e9a8cdc295838f1924c0ca903ae993456fddec07", size = 90991, upload-time = "2026-05-25T22:17:06.75Z" }, + { url = "https://files.pythonhosted.org/packages/14/88/1d21a36da8f5cb0fa49eafd4b169eba5608d57e75bbcf61845cbc6243216/httptools-0.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d", size = 208247, upload-time = "2026-05-25T22:17:07.843Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/cc4feea2945cb3051038f090c9b36bd5b8a9d7f5a894a506a8983e33fd1c/httptools-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5", size = 113064, upload-time = "2026-05-25T22:17:09.136Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a6/febbb8b8db0f58b38e44ad6cb946e6a255ae49b55f2e8543408fb7501ccd/httptools-0.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2", size = 523851, upload-time = "2026-05-25T22:17:10.106Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e4/f90a0df0b83beff265b7e3b65f2a4cefd95792d4be0ac3e16049f2acd3c2/httptools-0.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09", size = 518842, upload-time = "2026-05-25T22:17:11.218Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2d/0c9ac76dd2c893841fbf6498d6acec4f2442e1b7067f6e3e316a80e494e8/httptools-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a", size = 501238, upload-time = "2026-05-25T22:17:12.728Z" }, + { url = "https://files.pythonhosted.org/packages/ca/42/906adc91ae3a5fa9c59c0a2f21c139725bd7e5b41ae6acd485cd14123ebf/httptools-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745", size = 509567, upload-time = "2026-05-25T22:17:13.842Z" }, + { url = "https://files.pythonhosted.org/packages/05/0b/4240efeb672751ee5b9b380cb0e3fdc050bc05f68adc7a8aefc4fcd9a69a/httptools-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150", size = 90918, upload-time = "2026-05-25T22:17:15.155Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e5/8cfcabc5546e8022f168be28bcdaa128a240a0befdd03b59d558b4f18bd6/httptools-0.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8", size = 205148, upload-time = "2026-05-25T22:17:16.333Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0e/0fb14848c19a686c8062ff9067c1a48793e3224b47bc5b201535b6036fce/httptools-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c", size = 111368, upload-time = "2026-05-25T22:17:17.586Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/46f1cecf06b9bbde8e4b8c88034ac7908989e5ff7a3a388ef38392949c1f/httptools-0.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7", size = 486447, upload-time = "2026-05-25T22:17:18.564Z" }, + { url = "https://files.pythonhosted.org/packages/77/00/258bfc0837221f81d9725c45f9b948a6a6b2994a147a4fb66e85100c668f/httptools-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d", size = 482448, upload-time = "2026-05-25T22:17:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/04/ab/d1cef3b5523f4d272a70f42a776c3169a2dddfe3a54de4b2ce4a36341528/httptools-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681", size = 464460, upload-time = "2026-05-25T22:17:20.882Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/5d1d072442277bb2b3434e0e60690b8e8c23840ef7de8b6ea54040a536d3/httptools-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683", size = 471312, upload-time = "2026-05-25T22:17:22.085Z" }, + { url = "https://files.pythonhosted.org/packages/0d/66/b96623b27e51a68199ef4efdda0613cced9233fe3062ac74e50749c5ad37/httptools-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1", size = 90117, upload-time = "2026-05-25T22:17:23.074Z" }, + { url = "https://files.pythonhosted.org/packages/1a/12/fa3fbf5f9517b273edea2dc982aa82a8c634091e67c590792b729017bc6f/httptools-0.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6", size = 206183, upload-time = "2026-05-25T22:17:24.004Z" }, + { url = "https://files.pythonhosted.org/packages/30/fc/5e7c4cb443370f2090a3aba0453a07384d29ff66b7435bb90e77e1037599/httptools-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b", size = 112079, upload-time = "2026-05-25T22:17:25.216Z" }, + { url = "https://files.pythonhosted.org/packages/ba/53/771bd891eb0f236f32145d6a1775777ec85745f3cc983a1f23d1a3b8ddfe/httptools-0.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0", size = 481596, upload-time = "2026-05-25T22:17:26.186Z" }, + { url = "https://files.pythonhosted.org/packages/62/42/94e15bc68ce3d423243c45d7f1b0c7561f13844f97dc52ae23182fb65628/httptools-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e", size = 480865, upload-time = "2026-05-25T22:17:27.542Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7c/fe2980fc03723272e30f135b62360b075f513dfe7cc73aef36c7f04012bd/httptools-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b", size = 463189, upload-time = "2026-05-25T22:17:28.546Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/47fc5fff68acd1bfa20b4734059c9a06cadb88119dcd5258b5b0d21d91c8/httptools-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0", size = 466610, upload-time = "2026-05-25T22:17:29.816Z" }, + { url = "https://files.pythonhosted.org/packages/60/bd/07b13c93ffd9bec9546e0d43f8e19378dd696dbd278511406bc07371ef1f/httptools-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527", size = 92705, upload-time = "2026-05-25T22:17:31.133Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c4/121648f68ce066d7bd762d6b6d97e620847642d38d54f3d90ff11d947629/httptools-0.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568", size = 215023, upload-time = "2026-05-25T22:17:32.401Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b0/312a062ae741ae3e8baa8c8bf20be81b2e67337b259ab4349bebc7b6142e/httptools-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b", size = 117405, upload-time = "2026-05-25T22:17:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/fc/37/fccd705f795386bb05bf413012fecff2a33e5aa8c2f069096de3e9fd8702/httptools-0.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca", size = 558497, upload-time = "2026-05-25T22:17:34.732Z" }, + { url = "https://files.pythonhosted.org/packages/bd/39/f172e8003576de35f5ba77ff417cf0e34429d35dc014deef15afa337a72c/httptools-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f", size = 571585, upload-time = "2026-05-25T22:17:35.813Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b9/f5564760af99f3dbbf3f9104dc00e5da27e96cf433c6bdcf77617f70bf3f/httptools-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d", size = 543297, upload-time = "2026-05-25T22:17:37.08Z" }, + { url = "https://files.pythonhosted.org/packages/99/67/8d9f2c313618e161b82f3873188e7196126da1d6e29688df40eb3997c77a/httptools-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081", size = 539535, upload-time = "2026-05-25T22:17:38.032Z" }, + { url = "https://files.pythonhosted.org/packages/48/63/b906c01e53f50d432c0defe43ce52764a111dc1bdd028bafbeb54dcfd008/httptools-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77", size = 108209, upload-time = "2026-05-25T22:17:39.473Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +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 = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, +] + +[[package]] +name = "julius" +version = "0.2.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "torch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/c6/5c2aea2b11ead1680bb5b17c996def31f8ccade471cada37cdb93855e06f/julius-0.2.8.tar.gz", hash = "sha256:d691e651200930affea4f6c849c26e95fec846087281ae3d1a7757eac90253d5", size = 48081, upload-time = "2026-06-03T16:05:34.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/43/efdb0bcb07c47826fa55857cec0deb743f74cd83b6ba5ec9e413505a72e6/julius-0.2.8-py3-none-any.whl", hash = "sha256:6891235cbc355e629d839f87489bff8ca46e57a0e7cc35abb909c7a2aa538c25", size = 21819, upload-time = "2026-06-03T16:05:33.443Z" }, +] + +[[package]] +name = "lameenc" +version = "1.8.4" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/e4/8b80bc6e98b20e1a51a395d9dd9fe64ba1ac7a38d4bfa464445eed37ee08/lameenc-1.8.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e76adf8975bce5748d45bef3c520041c684093b76528fcfc773c3412b413ae5a", size = 189607, upload-time = "2026-06-27T15:03:06.977Z" }, + { url = "https://files.pythonhosted.org/packages/9a/d5/9b15afafe35d4815a356e62885a9190aefe04a16f5f83bf0a1ef290f82e9/lameenc-1.8.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:abedb78eebd63a226d1fdf8c75c2cb0d1b4df3d1227585e3e8d5f6b9cff22cb2", size = 183347, upload-time = "2026-06-27T15:02:53.154Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b1/58dd0e1c374bb65d70393e44c13fdbf7284e4cf33e7f453ac137abe1a9b7/lameenc-1.8.4-cp311-cp311-manylinux1_i686.manylinux_2_34_i686.manylinux_2_5_i686.whl", hash = "sha256:ad4e21fba6715460be492a64279097a979aa42cf07f7ef05981ccc4fac5063b2", size = 222667, upload-time = "2026-06-27T15:02:44.925Z" }, + { url = "https://files.pythonhosted.org/packages/73/18/ac32c846addcbcc66692860643aff57f83ed0d491fdedd6d4832aa23ba9a/lameenc-1.8.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e6cfafe7626aca3ec81d734b99293ec6bf59843378fd11c39798973d2e3351b", size = 252876, upload-time = "2026-06-27T15:12:16.966Z" }, + { url = "https://files.pythonhosted.org/packages/31/bf/934d0f584504c0556abc63437f80902e654a2aa3214f6f66309632a25a4d/lameenc-1.8.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_34_aarch64.whl", hash = "sha256:4163b7319680b6be7914cf8020c459869c619e0e99666dacd7e6ba0fd424d552", size = 252721, upload-time = "2026-06-27T15:07:57.148Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ff/6566199323ec55881b6eb33f40262f5c4a1bad95264c36652fbc48fef8e7/lameenc-1.8.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:59c383139afcb35dddf04abac6302a35a3f1d40407d83e4622a56df06f74bf5d", size = 248338, upload-time = "2026-06-27T14:58:54.791Z" }, + { url = "https://files.pythonhosted.org/packages/b8/fc/f9d6baf458687b67e4077c7b5577022112d1df1b1f37125b0754226a1fda/lameenc-1.8.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_34_x86_64.whl", hash = "sha256:a94ccc4c2f6e47d291303c769811bb63ca9cf68b0e7e4bb3b9b257362db1c27b", size = 248405, upload-time = "2026-06-27T15:02:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/04/d7/d56d8d2dedd9c700adf1d41259e50ceb8158ba296cdb5751dd571665af30/lameenc-1.8.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:277ba63533f2c04a39842e50b44ec855bc6958e91924d973de8ac4b7ef9a3883", size = 267280, upload-time = "2026-06-27T15:06:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/96/63/0f0933ce0ec0c9073ae0175c02628e56a27296ccf56f3bd658d10d96aab5/lameenc-1.8.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:043147260caf0c807270e5a3a157cb9008acb545eb66d92e4c5d3dd9e99c0fc6", size = 272218, upload-time = "2026-06-27T15:01:14.966Z" }, + { url = "https://files.pythonhosted.org/packages/62/f0/0118a59a26547a16323deb2d2a21931a329ab0b6bff02d763316291643c0/lameenc-1.8.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:000018fc35ab4ee4f42114d46e7160a12a1dc09cfef5ba24c6fa58ab2b4508b6", size = 199978, upload-time = "2026-06-27T15:02:45.851Z" }, + { url = "https://files.pythonhosted.org/packages/46/72/c6487a4d8f269f02a2de1deda7648ad03b193021c064ad9bedcabc0f52d4/lameenc-1.8.4-cp311-cp311-win32.whl", hash = "sha256:664af1b0b0b3dad43b6e8b5d297300b187de043f7209f59a19aa7ce03a35b8d9", size = 124668, upload-time = "2026-06-27T15:03:39.973Z" }, + { url = "https://files.pythonhosted.org/packages/70/9b/d6536b83d688f87150b6e6f2a57a7d3eb0dcb84efd4974605febc4d5c513/lameenc-1.8.4-cp311-cp311-win_amd64.whl", hash = "sha256:28e51e725de35fe9492cfeb83f19e5f676765342139794e50d5d5e3827c124ff", size = 153003, upload-time = "2026-06-27T15:03:48.708Z" }, + { url = "https://files.pythonhosted.org/packages/41/15/1a74db8285788ef2996397a6af32f22ae1b3d15715865d3ad9ef5cb8448e/lameenc-1.8.4-cp311-cp311-win_arm64.whl", hash = "sha256:42ba49928c43af4c362eeb288c98870940df0bfbf4b124871a4c88d16746d74c", size = 131477, upload-time = "2026-06-27T15:03:37.145Z" }, + { url = "https://files.pythonhosted.org/packages/e7/41/afa8b9bd15ebe757b8a1029b1f44b0caa94252dd12537d78a81c360ad069/lameenc-1.8.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8482f68a0910606efc182f1858fef8655681d9d29c8edc9fa5c36acf74819118", size = 189628, upload-time = "2026-06-27T15:03:08.34Z" }, + { url = "https://files.pythonhosted.org/packages/4b/bd/d64e49025090c1971eb085076a40d82f3fcd8339f2a2a4e1e224bd9aa482/lameenc-1.8.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fb0d5bb76b09d8bf4e27f4824a72e4acd659bd4ec8dac2879fd5744f3d6d88fc", size = 178226, upload-time = "2026-06-27T15:02:59.939Z" }, + { url = "https://files.pythonhosted.org/packages/a3/1a/fa4d2e4df30b6322a806da58b214c5c8de30e4136027dddbbaa1238e5c1c/lameenc-1.8.4-cp312-cp312-manylinux1_i686.manylinux_2_34_i686.manylinux_2_5_i686.whl", hash = "sha256:43500c41c51a88bdca9b4ee85c5764d4c0d8c5b1d1cb9cc35c2449fc2e0412f9", size = 221785, upload-time = "2026-06-27T15:02:46.71Z" }, + { url = "https://files.pythonhosted.org/packages/7f/80/9f1ae88f9dc02b6a9ad53ab687c3e13077bb81f3f452bb59f17b42318ba0/lameenc-1.8.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea7a7968b20535934bc11caca3d23b12e972de6e02f31bdc6a9e206c198cfd1e", size = 251884, upload-time = "2026-06-27T15:12:18.347Z" }, + { url = "https://files.pythonhosted.org/packages/98/4a/f5856aa2362feb8afc1a9e51d81a946b82413b465f5577943984dafab256/lameenc-1.8.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_34_aarch64.whl", hash = "sha256:606ee90e18b70b0134c410fe21db11e31bc539e1da1a2c298d90889878766552", size = 251631, upload-time = "2026-06-27T15:07:58.681Z" }, + { url = "https://files.pythonhosted.org/packages/49/98/ced7da98fb0c149e80d3a5a97546b5abcec6a06f4187cc8842a737107487/lameenc-1.8.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00d619c0a617f66feccbbd2fa9ed3857958ea503f9fe0038cb8b1d950b8b6452", size = 247546, upload-time = "2026-06-27T14:58:55.928Z" }, + { url = "https://files.pythonhosted.org/packages/48/03/1d153252a5aa9093a461b3d013b1e8d383806f6c8c59c7f65c6928197aaa/lameenc-1.8.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_34_x86_64.whl", hash = "sha256:18ba38c49759e217dd6fecf56ef92eab2a24f0a0d87ae4c3564ce4748d75b166", size = 247480, upload-time = "2026-06-27T15:02:59.079Z" }, + { url = "https://files.pythonhosted.org/packages/24/5c/f7f73b6ed2a46d149b7f8a2046c26e61e2cd4ac248f628cebce300abbf31/lameenc-1.8.4-cp312-cp312-win32.whl", hash = "sha256:513b5163b30581350be6c3e6adb58fd63ab1573ee534f5e9270655f3ffe63562", size = 124729, upload-time = "2026-06-27T15:03:41.39Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d1/b4b08b1c27b4991052db2fae3082100a6317fef34873bbeb121809315b22/lameenc-1.8.4-cp312-cp312-win_amd64.whl", hash = "sha256:33854f5b479cec81679860c8d67225e2ab3a31a0bde0bdf49b55e2bd6ee1923e", size = 153045, upload-time = "2026-06-27T15:03:40.24Z" }, + { url = "https://files.pythonhosted.org/packages/8b/bd/ccbf35970373ab076e5036c1f14670eb13ed05bf4dbb2fbdfefe35e8b812/lameenc-1.8.4-cp312-cp312-win_arm64.whl", hash = "sha256:e72e10ea0240bcc46e05df9dd4979116e74e183a5983cd0dcb14ff5315444649", size = 131452, upload-time = "2026-06-27T15:03:36.726Z" }, + { url = "https://files.pythonhosted.org/packages/f9/9c/608f3e1daf71203037fb3c04ff714fd369363d44d3a54d7fe21dbd307025/lameenc-1.8.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:78d8cdb3175e7c55a34c705c101a9e6483ae18572be22a6066aa4ef359df68f7", size = 189620, upload-time = "2026-06-27T15:03:07.299Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f7/be59571f5ad29ad9a02d2e3fb69668ae06f5ea9ae1742fcda656e58de62f/lameenc-1.8.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:05f1034b40d139a043c0ec877e968230dbc0945f320427d662d457277ab9bc4a", size = 183358, upload-time = "2026-06-27T15:03:04.675Z" }, + { url = "https://files.pythonhosted.org/packages/fd/62/70c196a516b38bf7fb3529e1c7621dbe8b05cf12c53eecda2628d9e5234d/lameenc-1.8.4-cp313-cp313-manylinux1_i686.manylinux_2_34_i686.manylinux_2_5_i686.whl", hash = "sha256:f3279d497a21395378e30cbf632bd40606c292e0f39d152e237ffb429cab3c8b", size = 221854, upload-time = "2026-06-27T15:02:48.374Z" }, + { url = "https://files.pythonhosted.org/packages/4e/1c/3a5863b8c8e2051ecce855a38099757218f8c0b2a2515015ce93519ff134/lameenc-1.8.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9ce4baad7f0516682a91aa11d1e8483fe1996640c9a8c0e667ec3aec65a6fc4", size = 251967, upload-time = "2026-06-27T15:12:19.877Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f6/ef38b5233ebddc05bae9ef5fe31e909f422b41a5a8e45073133fbf8fe191/lameenc-1.8.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_34_aarch64.whl", hash = "sha256:c3496f6e68fc6441b0f6972acab9298de85c2013f888f80dd69c41a4976470bc", size = 251714, upload-time = "2026-06-27T15:08:00.185Z" }, + { url = "https://files.pythonhosted.org/packages/21/ab/61087872800c15f91c5e50c5331b139c4b55b62cbb3fbd25aeb87052e752/lameenc-1.8.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e5b46a8e4ebf3dd495afc05fc8efcda24eac17b386e2c60b0d2e708d266c154", size = 247632, upload-time = "2026-06-27T14:58:57.199Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2b/96dfcb4947b2fe558791009d621c831972b13dc3f4ab489d62585cd81d16/lameenc-1.8.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_34_x86_64.whl", hash = "sha256:7e08ab42b8b6c2467c386e1ebb62fec8dae00cbb25d803d25e14bffd46fc9087", size = 247592, upload-time = "2026-06-27T15:03:00.536Z" }, + { url = "https://files.pythonhosted.org/packages/44/6c/d950bfcb0b8803ca281d5b72d1be7d0a045830ccda9a41fda86dd4c57669/lameenc-1.8.4-cp313-cp313-win32.whl", hash = "sha256:faf3926600c1f6ed577984e15647e5e459cedd9c929953acb70d605a2847b94e", size = 124725, upload-time = "2026-06-27T15:03:47.476Z" }, + { url = "https://files.pythonhosted.org/packages/53/aa/673a0c57d2e7ae5d800a2a43024d5ac1660ee26c114149e26a4188be93c2/lameenc-1.8.4-cp313-cp313-win_amd64.whl", hash = "sha256:7db3df4133d7b39f2f09ad684bf0a7a92c2d11117a0afc5db5cb152e48025b63", size = 153036, upload-time = "2026-06-27T15:03:46.669Z" }, + { url = "https://files.pythonhosted.org/packages/a8/23/5ade982d5d285b30144c7feb55a8680f2a883d14477046b44ec33c2cdac3/lameenc-1.8.4-cp313-cp313-win_arm64.whl", hash = "sha256:a9c40d7b054c2e8d816a95912268de52b7d3f5f1da250c73b611849c5159d072", size = 131448, upload-time = "2026-06-27T15:03:48.732Z" }, + { url = "https://files.pythonhosted.org/packages/13/57/44735025842e06e5e00f37049585df1ab45922b91d874bcce85110dc9deb/lameenc-1.8.4-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:55e468c75354fd3a1874282d4b23b605137025dca9b024bb8be8f4e91c5169e5", size = 189543, upload-time = "2026-06-27T15:03:23.256Z" }, + { url = "https://files.pythonhosted.org/packages/97/d6/14e15129caa7cb4d2cb5c6b2b030d0bbc87ab9c7224be2a84d88997b3e78/lameenc-1.8.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:859fa9f05e0c7e825efb72431f8243bcc4318c71ff3b4d57c7cebaed6fcadb65", size = 183350, upload-time = "2026-06-27T15:03:11.532Z" }, + { url = "https://files.pythonhosted.org/packages/da/35/818502b8e55b4cd9f7743500015e9ce07f01d474acdade0b0bd5e5ad3221/lameenc-1.8.4-cp314-cp314-manylinux1_i686.manylinux_2_34_i686.manylinux_2_5_i686.whl", hash = "sha256:92dae11d2fd422c3c310900893edd3e20d538741959c7cd426d91af2cf18fe27", size = 222018, upload-time = "2026-06-27T15:02:49.686Z" }, + { url = "https://files.pythonhosted.org/packages/de/9c/6fd42cb5c8fde74793042a16c3278a39c814f00ce37797ea61b642caeaff/lameenc-1.8.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29fa3dfb57b3d1ef021b2c9b9b940e2502d139bcf0c84153cd0f57c4506b856d", size = 252091, upload-time = "2026-06-27T15:12:21.482Z" }, + { url = "https://files.pythonhosted.org/packages/34/65/66211814595cd9ce2bbf8c7cea345c947fdae90f87573ccf082a2bbc525b/lameenc-1.8.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_34_aarch64.whl", hash = "sha256:627588bc0a2520b33e87d7966bedb1138b724f18c0a5d24a2a3a12de17351fad", size = 251856, upload-time = "2026-06-27T15:08:01.728Z" }, + { url = "https://files.pythonhosted.org/packages/36/d6/224f9055296dfd16e44da364220167c2612402906fcc53e9e882d6bc72cc/lameenc-1.8.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6527a8ae8ac078010a1fecc697145e7be1bb163cd5092b5c32d4332b6430886", size = 247729, upload-time = "2026-06-27T14:58:58.417Z" }, + { url = "https://files.pythonhosted.org/packages/43/6c/2298da206cdeac946cafc07d9e04d48e457874c96e6f5c8676cce39c83a0/lameenc-1.8.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_34_x86_64.whl", hash = "sha256:d44282c566712e42aee1624b5e406a9f277ae5395b729338bd20d844a98eb770", size = 247696, upload-time = "2026-06-27T15:03:02.216Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fa/30f3b02d8da0f209341b98a01f9918a7e2c68dee07949279a008f7f7647d/lameenc-1.8.4-cp314-cp314-win32.whl", hash = "sha256:31ab1bf3b191995293c1e085b43e3d78046341a156328d222d9a4d5eb3e149e3", size = 128577, upload-time = "2026-06-27T15:03:54.3Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c7/3132e584e9df8196013bd8104e17ca3247e18d5ebfe9c9369878fc8a5924/lameenc-1.8.4-cp314-cp314-win_amd64.whl", hash = "sha256:74ddfa8ba265924f958c1135dacc62345fcee05a9449b26a902541cfa9b9857e", size = 157385, upload-time = "2026-06-27T15:03:44.671Z" }, + { url = "https://files.pythonhosted.org/packages/bf/a9/d88809cb11105984bd8fb17c98cae626f8ddf7a27e1e146fb89028cb81bf/lameenc-1.8.4-cp314-cp314-win_arm64.whl", hash = "sha256:d44397967f9b10daa3b6941d20e7035ec8d7c5168f1a108f831c6cd0de5ccd3c", size = 136928, upload-time = "2026-06-27T15:03:35.9Z" }, + { url = "https://files.pythonhosted.org/packages/39/15/376104b0580a45e4da36c413850c979b58d602a9c2e08271deb40f4d5c89/lameenc-1.8.4-cp314-cp314t-manylinux1_i686.manylinux_2_34_i686.manylinux_2_5_i686.whl", hash = "sha256:239741e14b715676326a4b340fe475b6f1007ecc31d50c38fba96736536e26b0", size = 223798, upload-time = "2026-06-27T15:02:51.011Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3d/e693d99d943e0741780e917368152f8b0fe054311dbf6278ddb777bcd254/lameenc-1.8.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61ee4980f099b3791322591a150e2efe3468f5f2cf145af0c55c866f11708cf6", size = 254301, upload-time = "2026-06-27T15:12:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/64/1a/25fe55ae2a2e376c6ba1c40d4085ce2308ad32c125efc93d04b138c09639/lameenc-1.8.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_34_aarch64.whl", hash = "sha256:694afa6da2d89856017493bb1089283293988ba6522bad28e23697850569335e", size = 253929, upload-time = "2026-06-27T15:08:03.077Z" }, + { url = "https://files.pythonhosted.org/packages/32/af/668d9fc052852dd04fcb7093d55bd88484c2821bc0132adb75b017ede7c6/lameenc-1.8.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6a4948b98574c025e8902af0aaba905ce9bf032a0b6ce6a578b64817611860e5", size = 249448, upload-time = "2026-06-27T14:58:59.78Z" }, + { url = "https://files.pythonhosted.org/packages/b7/d9/be995262968580b08e88e47d2e7a0192dce209009d554569a50a8335674c/lameenc-1.8.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_34_x86_64.whl", hash = "sha256:970685ae4ac246dccc177e3dad16a27187582cd4cdc57208e894e6bf860699d7", size = 249276, upload-time = "2026-06-27T15:03:03.77Z" }, + { url = "https://files.pythonhosted.org/packages/17/65/90a62ce8cb745daaab3883175cde26f19634b066c4305fbfabc0b1135ffa/lameenc-1.8.4-cp315-cp315-manylinux1_i686.manylinux_2_34_i686.manylinux_2_5_i686.whl", hash = "sha256:63bc671e3ca8a23654930af46251d848d67c4e47a566edd37f97795ce49bb82f", size = 222718, upload-time = "2026-06-27T15:02:52.234Z" }, + { url = "https://files.pythonhosted.org/packages/4f/08/1f263ff43d4af81e62ad6c85401049f6519dd1e8539c349281c91e2235bd/lameenc-1.8.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:389b4210f47e68cf031c00db6f2cf41b517f6c5b00a8463e2c0dc1bbb3350394", size = 252497, upload-time = "2026-06-27T15:12:24.79Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7d/70f649abc1af4b9844c063dd51dcbec3e56b61373921d35e40976278c460/lameenc-1.8.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_34_aarch64.whl", hash = "sha256:e668d65f85b73d250b82c3a925c503c5b41ee3fe2ebff4255d620ebc8dff0148", size = 252265, upload-time = "2026-06-27T15:08:04.567Z" }, + { url = "https://files.pythonhosted.org/packages/87/e1/2e4acbce8383b324d887eb24a78d2f9b815ee5a4c36fa6198b62d45c9664/lameenc-1.8.4-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c00703b1c7fb7c2aecf453e750f0011914753ddbe529ec54ed98f53b8adba256", size = 248060, upload-time = "2026-06-27T14:59:00.926Z" }, + { url = "https://files.pythonhosted.org/packages/c0/82/bb07020a50b140bdcc1a77c7f5b478560e80a50f58ca4b5feac85c059305/lameenc-1.8.4-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_34_x86_64.whl", hash = "sha256:1daa7739fb469558d2786909cee9e9e76a9f53fa93f8c1825acdc6c2d8192570", size = 248081, upload-time = "2026-06-27T15:03:05.229Z" }, + { url = "https://files.pythonhosted.org/packages/25/c4/23f73c2a159083cccddbd4b69c1a59f2c97b239c4f8fe638daf753486a4b/lameenc-1.8.4-cp315-cp315t-manylinux1_i686.manylinux_2_34_i686.manylinux_2_5_i686.whl", hash = "sha256:08ec5c10472dd153a75b17a52b402b5d62628fa054d701fc4a27ddd86e037351", size = 224228, upload-time = "2026-06-27T15:02:53.54Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f5/00858b041b3dbdd833f3e28fed316bf73e2d0bc1519b560b5320af9679bd/lameenc-1.8.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0bf1463f79c7965922dd0604dfaedd636e9e74acefb21ec254419f2b42cf6a4e", size = 254707, upload-time = "2026-06-27T15:12:26.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/ae/3f091c3090e5dc093f781134a73d6ae41ad2f46426bfdb7bb1d884c47bae/lameenc-1.8.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_34_aarch64.whl", hash = "sha256:2f8ae9b47b02c327ac4ab0f5378dafc1f7b5bf0bd30b90fa81033ee71f0005d8", size = 254307, upload-time = "2026-06-27T15:08:05.914Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f0/45a32cd5f41cc8462ecd97e47d651bf525e10ba1f7c71de3c5b18efcfdbc/lameenc-1.8.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8aacb9f345ff0e6cab137d0d8436c5d5712b332c4998f42d167fb5677bee83e", size = 249855, upload-time = "2026-06-27T14:59:02.08Z" }, + { url = "https://files.pythonhosted.org/packages/98/69/818d51a0c2004c26fd6c118eecb9508d6b672d22f813e11bf4586894be92/lameenc-1.8.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_34_x86_64.whl", hash = "sha256:7f83753a35babf2e70d1d511c3fdde0ceedf1af4977b705cb591b8da47bb457d", size = 249696, upload-time = "2026-06-27T15:03:06.985Z" }, + { url = "https://files.pythonhosted.org/packages/37/6b/e89a09b7806ac23d0a925a8031ad449d8a41a5f74713c9325599e1ac8439/lameenc-1.8.4-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_34_i686.manylinux_2_5_i686.whl", hash = "sha256:4244d78ec6915c7b43532e691efbb1eadc347509b90abcd6066e1f92799e1088", size = 209690, upload-time = "2026-06-27T15:02:55.16Z" }, + { url = "https://files.pythonhosted.org/packages/7c/43/35863834d0526a9197961b6b0e86d9e1ed2110129fbd59603aabac29737e/lameenc-1.8.4-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cdb498504559f9bfee58f65347343c0f0aa11bd5537d59cb0853a9e22d45a65f", size = 238284, upload-time = "2026-06-27T15:12:28.541Z" }, + { url = "https://files.pythonhosted.org/packages/b0/d2/0a06cee4ebec732a0d89a3467093e268078c2fc96c52cdbd1989e433d542/lameenc-1.8.4-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_34_aarch64.whl", hash = "sha256:e24a0358e1bc8f791c5b861f458ba568e7bdc42a9912b7415bd6f15c2df45388", size = 238274, upload-time = "2026-06-27T15:08:07.472Z" }, + { url = "https://files.pythonhosted.org/packages/0f/d4/086da93a95a53b641511d46f7f757f8c3a46041079a6250dfe1c23a4a53b/lameenc-1.8.4-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8dda5242e426d73ce915c765147dc0b9fc7f4b639745a1a45dabde0104f88595", size = 233700, upload-time = "2026-06-27T14:59:03.197Z" }, + { url = "https://files.pythonhosted.org/packages/86/1c/2e18b64d729d42d28bcce4f974d9f2c0bc69749abe7c8733a3e1368f10a0/lameenc-1.8.4-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_34_x86_64.whl", hash = "sha256:2d2fd072c981e85777f3eb3c6f2e28da0a276934830bda8c9a32d64ffdd52130", size = 233700, upload-time = "2026-06-27T15:03:09.375Z" }, +] + +[[package]] +name = "lazy-loader" +version = "0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/ac/21a1f8aa3777f5658576777ea76bfb124b702c520bbe90edf4ae9915eafa/lazy_loader-0.5.tar.gz", hash = "sha256:717f9179a0dbed357012ddad50a5ad3d5e4d9a0b8712680d4e687f5e6e6ed9b3", size = 15294, upload-time = "2026-03-06T15:45:09.054Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl", hash = "sha256:ab0ea149e9c554d4ffeeb21105ac60bed7f3b4fd69b1d2360a4add51b170b005", size = 8044, upload-time = "2026-03-06T15:45:07.668Z" }, +] + +[[package]] +name = "librosa" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "audioread" }, + { name = "decorator" }, + { name = "joblib" }, + { name = "lazy-loader" }, + { name = "msgpack" }, + { name = "numba" }, + { name = "numpy" }, + { name = "pooch" }, + { name = "scikit-learn" }, + { name = "scipy" }, + { name = "soundfile" }, + { name = "soxr" }, + { name = "standard-aifc", marker = "python_full_version >= '3.13'" }, + { name = "standard-sunau", marker = "python_full_version >= '3.13'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/36/360b5aafa0238e29758729e9486c6ed92a6f37fa403b7875e06c115cdf4a/librosa-0.11.0.tar.gz", hash = "sha256:f5ed951ca189b375bbe2e33b2abd7e040ceeee302b9bbaeeffdfddb8d0ace908", size = 327001, upload-time = "2025-03-11T15:09:54.884Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/ba/c63c5786dfee4c3417094c4b00966e61e4a63efecee22cb7b4c0387dda83/librosa-0.11.0-py3-none-any.whl", hash = "sha256:0b6415c4fd68bff4c29288abe67c6d80b587e0e1e2cfb0aad23e4559504a7fa1", size = 260749, upload-time = "2025-03-11T15:09:52.982Z" }, +] + +[[package]] +name = "llvmlite" +version = "0.48.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/a0/acc8ffcd5bdc63df0097e22c719bfcd61b604358343089313a8aebbb24ab/llvmlite-0.48.0.tar.gz", hash = "sha256:543b19f9ef8f3c7c60d1468191e4ee1b1537bf9f8a3d56f64c0ddd98de92edd2", size = 184016, upload-time = "2026-07-02T20:20:05.308Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/55/595981f14fbae9ba966feb12af552b1fe69889e44e64ac883a731ed335e0/llvmlite-0.48.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:56a7e24607d3f02d7b1bae8d29c7e1e423d53143d68b072999777f19678fe77b", size = 40480651, upload-time = "2026-07-01T18:41:18.438Z" }, + { url = "https://files.pythonhosted.org/packages/26/08/0109d1b9cb3f4603f3890e30bc66c65332b79185f12a045343b2ae431f67/llvmlite-0.48.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6fa532d6bb3fd3f0803567c736401c54aecfe1a396d3ad25d2440d220e09f0e7", size = 59890118, upload-time = "2026-07-01T18:41:28.184Z" }, + { url = "https://files.pythonhosted.org/packages/02/eb/c5281be180c789cdffbf45b671884c57d7e61345ef3b0f643a4965e108e8/llvmlite-0.48.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:979a66a3f28a02565383ff463527dce78e9b856298872a361283132488e83591", size = 58343458, upload-time = "2026-07-01T18:41:23.397Z" }, + { url = "https://files.pythonhosted.org/packages/aa/f7/b3222b13f2d424dae3c9e63fde476af25ebccf1f3faf0b52d1b79fc15c70/llvmlite-0.48.0-cp311-cp311-win_amd64.whl", hash = "sha256:efaee0276e5e17c2b99b92e0c974bd484ef5977cf5dbc9168e82b71578edb47f", size = 41864734, upload-time = "2026-07-01T18:41:31.932Z" }, + { url = "https://files.pythonhosted.org/packages/92/a2/28696a9e61e245d1a79816d29d106692a90a2b6e7d78c98b326db70827af/llvmlite-0.48.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:d66c3beb4209087ddd4cf4ed2a0856b6887e6a913bdcf1aacfec9851cf2cba4e", size = 40480651, upload-time = "2026-07-01T18:41:35.694Z" }, + { url = "https://files.pythonhosted.org/packages/80/f2/72409351db66d0a317ec5087e076f31fb7b773a640db8a90ce6b5cac9edd/llvmlite-0.48.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:416fa4c2c66c2c6dc6d0a402648c19206e548efa0aa1eff01ad5cdad0af8217d", size = 59890118, upload-time = "2026-07-01T18:41:44.886Z" }, + { url = "https://files.pythonhosted.org/packages/3a/27/5ae2f3722606360480707adb47f001ad89df8251d06b14ee80336e660b66/llvmlite-0.48.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5e5a5131045b72345c71062ea1a91910dde913792b6c9b28ebb2c1c0a712e98", size = 58343459, upload-time = "2026-07-01T18:41:40.306Z" }, + { url = "https://files.pythonhosted.org/packages/16/78/d824ffff7521cd140dc2006e44ce2bc82e64b48d1b32e90e956308c85a74/llvmlite-0.48.0-cp312-cp312-win_amd64.whl", hash = "sha256:d45c7541a80934ec6d8ab0defe67439494ecd2193cbf852a44ba827808976ac1", size = 41865022, upload-time = "2026-07-01T18:41:48.663Z" }, + { url = "https://files.pythonhosted.org/packages/9c/23/fe9316d14626b42c73ef0b502e724705a6ee9450afe53759c0a99c37c2d7/llvmlite-0.48.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:a83a99ef0c05b4ccddf9b6218ed9fe84b653a0caf7c1d9dbe148d6d16c67f518", size = 40480652, upload-time = "2026-07-01T18:41:52.216Z" }, + { url = "https://files.pythonhosted.org/packages/1b/4a/90715fa12006d681270b08d881195b6fab3ec39572e048764a1f7f59fed7/llvmlite-0.48.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8761b9e522f55207e24424fcd98370289eec2710bf8e915c82d1053f642450dc", size = 59890120, upload-time = "2026-07-01T18:42:00.748Z" }, + { url = "https://files.pythonhosted.org/packages/70/5e/7b3e20d64650ca3c80af0cdb664ec4b575ec83d9d4dd05bea8bd31f9bbb6/llvmlite-0.48.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fe5cb59b2063bfa039dcb8ca6481c0181bf552f340d10dcf61d7996a665556e", size = 58343457, upload-time = "2026-07-01T18:41:56.41Z" }, + { url = "https://files.pythonhosted.org/packages/17/97/5a430055d1838cf1fb7a01cfa943300f5e4c026fc6333a522c5e4a03b0c1/llvmlite-0.48.0-cp313-cp313-win_amd64.whl", hash = "sha256:91c7e24e74cde3f02b88aa5acca678373f9e069f3b98531b3dbb3a142d9d10bb", size = 41865022, upload-time = "2026-07-01T18:42:04.57Z" }, + { url = "https://files.pythonhosted.org/packages/8d/8e/8170f2e0c217f88069c333d85bb976e536b332aecfcce606ddbdb249385f/llvmlite-0.48.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:321f1ac39b462603f0b589751aecf2d237d056f6d005749c1752b6f23ec3f074", size = 40480650, upload-time = "2026-07-01T18:42:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e1/05b50692b647cac3c18200ac485b04f342f00ed173c9cc46767274469a15/llvmlite-0.48.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:05f0103c8f2f96a37441337e3643863c01b8e83e530aff38960dcb383c54a065", size = 59890115, upload-time = "2026-07-01T18:42:17.805Z" }, + { url = "https://files.pythonhosted.org/packages/f7/c3/470b8c4ff9ae2db2f9cf5c3e73de76ed908a32788ae9eb5602d43e6a476b/llvmlite-0.48.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:37d66fae72802175b0bfe1ea06e624b51e2d7aee6c3c34bbd09739b8f88e8e0b", size = 58343457, upload-time = "2026-07-01T18:42:13.217Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2d/6a5171fb7236ac0895e1a02ccba3735bf291e8597239aa6421894d3c0ba8/llvmlite-0.48.0-cp314-cp314-win_amd64.whl", hash = "sha256:966dcab0a598e2bd8fb5f2cc082cf7b07bae564fc485a3a8692393caf986facf", size = 42986372, upload-time = "2026-07-01T18:42:21.483Z" }, + { url = "https://files.pythonhosted.org/packages/94/e3/7a93e09c9f94e637ca90209ceef0334a9a1d45b0bdb7c92ff922d25d6187/llvmlite-0.48.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7a5c413317050a1d67c34708bde97707f9b2257ef1017f7532d21fe7d9a9ff30", size = 40480654, upload-time = "2026-07-01T18:42:25.076Z" }, + { url = "https://files.pythonhosted.org/packages/27/98/a29133b4728671a175f7d616fab8b1c6e1d8c269d1523581d3160697bfb1/llvmlite-0.48.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d9f952dff6c350c529997423d4fa43abae9722a884ac7ffafc37a3af676e7db", size = 59890119, upload-time = "2026-07-01T18:42:33.88Z" }, + { url = "https://files.pythonhosted.org/packages/1a/cf/7aac11a1f1c7ec54b60c7f6814e87561fb6b55b2f290455d7941eb113420/llvmlite-0.48.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:054aa7d46595935565f276cf0c1659b4f10929c996dd4a606875fae26fba2a23", size = 58343460, upload-time = "2026-07-01T18:42:29.545Z" }, + { url = "https://files.pythonhosted.org/packages/db/41/b96f440c7df5ebba07872cad4e30fbc3560387755b1ea0b629adb76d5ca8/llvmlite-0.48.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d0b3c61aac83b42fb48cc96bffbf57c81b82b2aa92276b7ed6420c814629a99a", size = 42986383, upload-time = "2026-07-01T18:42:37.544Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { 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" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "msgpack" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/31/f9/c0a1c127f9049db9155afc316952ea571720dd01833ff5e4d7e8e6352dbb/msgpack-1.2.1.tar.gz", hash = "sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647", size = 183960, upload-time = "2026-06-18T16:13:52.594Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/6b/e9b1cdc042c4458801d2545ed782a95f3d6ba8e270cce8745b8603c7f748/msgpack-1.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:29a3f6e9667868429d8240dfd063ea5ffdc1321c13d783aa23827a38de0dcb22", size = 82812, upload-time = "2026-06-18T16:12:45.022Z" }, + { url = "https://files.pythonhosted.org/packages/0c/3a/dd518a1bf78ed1e9ad8afe57307c079a00eafe4b3068932a27ca1ea56b4f/msgpack-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aded5bdf32609dc7987a49bbbd15a8ef096193f96dd8bbeb791de729e650acf5", size = 82739, upload-time = "2026-06-18T16:12:46.025Z" }, + { url = "https://files.pythonhosted.org/packages/70/e0/7ba9e1542bf0771a27b8b37c1316e3f95ae9d748fd765284655c476ad4ef/msgpack-1.2.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:146ee4e9ce80b365c6d4c47073da9da7bcec473e58194ceee5dd7620ace77e06", size = 414233, upload-time = "2026-06-18T16:12:47.029Z" }, + { url = "https://files.pythonhosted.org/packages/03/8d/671d81534ea0e2b0e8a121be100020da09eb78861fe3aa8f3ef7dcd3bed1/msgpack-1.2.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a28d076ca7c82b9c8728ad90b7147489449557038bed50e4241eb832395169b4", size = 423843, upload-time = "2026-06-18T16:12:48.19Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b6/e5c737515ed1f166664b87601b532f58cbb73d8aa6a90b99f7c2c5037e8e/msgpack-1.2.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7d31c0ac0c640f877804c67cb2bc9f4e23dc2db97e96c2e67fa27d38283b41f8", size = 390772, upload-time = "2026-06-18T16:12:49.624Z" }, + { url = "https://files.pythonhosted.org/packages/a8/46/62ed8c2e87d7021eab19921594d961ef3aa3794eec76c716dc30f3bfd433/msgpack-1.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ff92d7feeaf5bc26c51495b69e2f99ed97ab79346fb6555f44be7dd2ac6503b", size = 409559, upload-time = "2026-06-18T16:12:50.936Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/59aa3887b860bbf43532835e192b1c388a17590d6068ae4f8b2bc74c906e/msgpack-1.2.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:779197a6513bab3c3632265e3d0f7cb3227e62510841a6f34f1eaa37efbb345e", size = 387838, upload-time = "2026-06-18T16:12:52.161Z" }, + { url = "https://files.pythonhosted.org/packages/09/11/f8563e471093420cf6478cb3271a0175d8402b82d879783d4035d2d03360/msgpack-1.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:67f6dd22fa72a93752643f07889796d62739a13415ee630169a8ce764f86cf9f", size = 421732, upload-time = "2026-06-18T16:12:53.556Z" }, + { url = "https://files.pythonhosted.org/packages/57/cf/e673683c4c6c90c1022b24c65af4b03eda72b182a1176ef6449069d66acc/msgpack-1.2.1-cp311-cp311-win32.whl", hash = "sha256:91054a783328e0ea7954b8771095705c8d2243b814743fbaadf14552c9c52c5d", size = 64091, upload-time = "2026-06-18T16:12:54.821Z" }, + { url = "https://files.pythonhosted.org/packages/3f/07/ca212739d179f9083bff2c7c08c24101c3555a334fadc2b876b18768a3ae/msgpack-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2eda0b7ebb1283a98d3e4492ac933c8af6aff59fd3df1c3ed024f536af4b1dc8", size = 70462, upload-time = "2026-06-18T16:12:55.898Z" }, + { url = "https://files.pythonhosted.org/packages/6d/be/6798347b425e26f35db82e69dd83c09716c856a3714e7bffc4c0860fd830/msgpack-1.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:6ee967f7c7e1df2890c671ff2ee51a28ded0efc95da3e507176dee881ce36c66", size = 65059, upload-time = "2026-06-18T16:12:57.053Z" }, + { url = "https://files.pythonhosted.org/packages/bc/dd/9e8cbd8f5582ca4b590336f2b91ee5662f6a6ca562b565abaf696a0f81ff/msgpack-1.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ef59c659f289eddf8aa6623823f19fa2f40a4029266889eac7a2505dd210c35", size = 83531, upload-time = "2026-06-18T16:12:58.249Z" }, + { url = "https://files.pythonhosted.org/packages/50/2e/ebdb85a8da151397a2790363676b7ed7c125924fe618e4c6d8befb0cc62c/msgpack-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3567748a5107cb40cdf66a275430c2f87c07777698f4bfd25c35f44d533258c", size = 82657, upload-time = "2026-06-18T16:12:59.396Z" }, + { url = "https://files.pythonhosted.org/packages/26/aa/753ad8b007b464e1d8aa0c8e650b9c5f4f725e658fc5ac8a7635c55b7f6e/msgpack-1.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60926b75d00c8e816ef98f3034f484a8bc64242d66839cef4cf7e503142316a0", size = 410634, upload-time = "2026-06-18T16:13:00.383Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/6adabd4f6d5e686f97dd02ce7fce3fe4cf672cbac36b8f67ff4040e8ad8b/msgpack-1.2.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:020e881a764b20d8d7ca1a54fc01b8175519d108e3c3f194fddc200bda95951a", size = 419989, upload-time = "2026-06-18T16:13:01.776Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cc/85039b7b0eb168aaad7383a23c97e291a11f08351cb45a606ce865e4e3f1/msgpack-1.2.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4202c74688ca06591f78cb18988228bd4cca2cc75d57b60008372892d2f1e6e6", size = 377544, upload-time = "2026-06-18T16:13:03.637Z" }, + { url = "https://files.pythonhosted.org/packages/ed/bf/35963899493b32030c85fc513b723ae66144ac70c11ebc52e889e16e3d99/msgpack-1.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8b267ce94efb76fbd1b3373511420074ee3187f0f7811bf394531de13294735a", size = 400842, upload-time = "2026-06-18T16:13:05.012Z" }, + { url = "https://files.pythonhosted.org/packages/a6/df/8e2ac970c8f99264cd9997d1c73df5466bc19da3301d7dc5500862a9b089/msgpack-1.2.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f1d0f8f98ade9634e01fb704a408f9336c0a8f1117b369f5db83dc7551d8b1", size = 374108, upload-time = "2026-06-18T16:13:06.232Z" }, + { url = "https://files.pythonhosted.org/packages/17/dd/fa8bd265110dfa51c20cb529f9e6d240a16fafe7e645004c6af2d01353ba/msgpack-1.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f02cf17a6ca1abe29b5f980644f7551f94d71f2011509b26d8625ce038f0df64", size = 414939, upload-time = "2026-06-18T16:13:07.478Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b9/8377a5ad8953fc0437c70cc98d9ae29f27fe5ac5109fbec0812085865735/msgpack-1.2.1-cp312-cp312-win32.whl", hash = "sha256:0c0d9802354507bcba62af19c17918e3eb437cc25e6f50657d511b5856a77aac", size = 64504, upload-time = "2026-06-18T16:13:08.822Z" }, + { url = "https://files.pythonhosted.org/packages/57/7f/ce1e377df7e62461fefd9eb23bfb93a4a523f40a517b377b8f844d836828/msgpack-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:5c24aa15d5963051e1a5c62b12c50cd705992502b5ec1f3bece6046f33c9fc24", size = 71421, upload-time = "2026-06-18T16:13:09.828Z" }, + { url = "https://files.pythonhosted.org/packages/8f/32/ebfe84c9929f08f188d56c7a2fd913406a9ddad76a634697c1c43b8112e6/msgpack-1.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:4227224aaec8f7fbcbfbd4272319347b2bb4030366502600f8c45588c5187b07", size = 64775, upload-time = "2026-06-18T16:13:11.056Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ac/dcddcab6f6c20ecb387ca5e980371cdb3f87ff69aeca388be97eebc4c074/msgpack-1.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a70e3cf2804a300d921bb0940426e35f4e489a23adfb77a808892241db0a064", size = 83151, upload-time = "2026-06-18T16:13:12.173Z" }, + { url = "https://files.pythonhosted.org/packages/64/71/fbcfa83a1d6a9c6091942d1cfd070962244664b87427a9a49a6897b1b219/msgpack-1.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:491cc39455ca765fad51fb451bf2915eb2cf41192ab5801ce8d67c1d614fe056", size = 82351, upload-time = "2026-06-18T16:13:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/e3/10/ddf7b06db879e8792d13934ddda09ff20bd2a583fd84c9b59aae9b0e650b/msgpack-1.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f310233ef7fb9c14e201c93639fe5f5260b005f56f0b29048e999c30935596cc", size = 407518, upload-time = "2026-06-18T16:13:14.233Z" }, + { url = "https://files.pythonhosted.org/packages/79/d3/36a46a8ed992b781acbc05928bd5bee3c810cb0c3563bf81a7b0c04a1a76/msgpack-1.2.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:787c9bebb5833e8f6fc8abca3c0597683d8d87f56a8842b6b89c75a5f3176e2d", size = 416405, upload-time = "2026-06-18T16:13:15.435Z" }, + { url = "https://files.pythonhosted.org/packages/f9/84/e8e9598b557c0ba6ddae901a73780a4c75ac667dddf59414b1e56a42fb34/msgpack-1.2.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc871b997a9370d855b7394465f2f350e847a5b806dd38dcc9c989e7d87da155", size = 376257, upload-time = "2026-06-18T16:13:17.022Z" }, + { url = "https://files.pythonhosted.org/packages/40/16/738fe6d875ad7e2a9429c165322a4ec088f4f273cdfae63d96a89c467961/msgpack-1.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:85f57e960d877f2977f6430896191b04a21f8901b3b4baf2e4604329f4db5402", size = 397469, upload-time = "2026-06-18T16:13:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/ca/be/6d5952df75a7f24f35833af764c3a6860780364cb3a0030beb8099e1b2b4/msgpack-1.2.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1233ee2dd0cefba127583de50ea654677277047d238303521db35def3d7b2e7c", size = 372802, upload-time = "2026-06-18T16:13:19.685Z" }, + { url = "https://files.pythonhosted.org/packages/e1/39/e2ef7dbf0473bcb8dc7c50bf782a892d67414877b63e47fc88eb189ef5e6/msgpack-1.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e3dc2feb0876209d9c38aa56cb1de169bd6c4348f1aa48271f241226590993e6", size = 411273, upload-time = "2026-06-18T16:13:21.028Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c5/133f4512a56e983a93445c836c9d94d88f3bc2e0980ff4b9e577bd8416ce/msgpack-1.2.1-cp313-cp313-win32.whl", hash = "sha256:6d09badf350af2be9d189184e04e64cf54ad93569ab3d96fca58bd3e84aad707", size = 64471, upload-time = "2026-06-18T16:13:22.293Z" }, + { url = "https://files.pythonhosted.org/packages/e2/98/577e10b055096a7dd40732358cabaf7180a20c79ed1dcdbb618e4b9deac7/msgpack-1.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:33f14fba63278b714efe6ad07e50ea5f03d91537aa6a1c5f1ceca4cf44013ca9", size = 71274, upload-time = "2026-06-18T16:13:23.455Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ee/0c0048e7cfbef23c6a94791b8959ab28155232e7956de8a305b5ff588f05/msgpack-1.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc5febcd4c99effbc02b528e49d6fd0760b2b7d48c05239e345a5fa6e743d9a", size = 64795, upload-time = "2026-06-18T16:13:24.687Z" }, + { url = "https://files.pythonhosted.org/packages/77/58/cce442852c6b9e1639c7c8ac8fd9143121cb32dab0f308df4d1426a8eb9c/msgpack-1.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:05f340e47e7e47d2da8db9b53e1bb1d294369e9ef45a747441309f6650b8351d", size = 83610, upload-time = "2026-06-18T16:13:25.724Z" }, + { url = "https://files.pythonhosted.org/packages/60/5c/15b4c7a0182f75ffa90751958ba36a9c01cafee367d49a3edc10ed140b01/msgpack-1.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:810b916696c86ef0deb3b74588480224df4c1b071136c34183e4a2a4284d7ac7", size = 83138, upload-time = "2026-06-18T16:13:26.781Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a6/99e58722feaffc5f2fbcc0c8c0d1451ab9f84097f7af87291b46af2390f4/msgpack-1.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca0dacff965c47afdc3749a8469d7302a8f801d6a28758d55120d75e66ce6889", size = 406090, upload-time = "2026-06-18T16:13:28.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/03/8c63e8cf52958534ef688625965ab04c269a6cadd8caef16758b380a821a/msgpack-1.2.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e2bf9280bceb5efca998435904b5d3e9fdbcc11d90dc9df30aec7973252b720", size = 412106, upload-time = "2026-06-18T16:13:29.427Z" }, + { url = "https://files.pythonhosted.org/packages/63/d2/155d9e71b40e41fd934bc0c48b9b2770f22263e1ac20aad8e29fdca7be3f/msgpack-1.2.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6c4be5d1c02a42b066ca6ddb71adf36432868fdcdb6ee87e634e86e0674190", size = 374851, upload-time = "2026-06-18T16:13:30.631Z" }, + { url = "https://files.pythonhosted.org/packages/98/48/deaf2326262a8d5ea3295ce9649912ecd3f551ba7ec8e33c665d2ba583f3/msgpack-1.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec0e675d59150a6269ddc9139087c722292664a37d071a849c05c473350f1f2d", size = 396168, upload-time = "2026-06-18T16:13:31.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/2a/b4410f906c2ec0008f1608d3ab5143afc3ad3f4e6da0fed3ea2231d0bef4/msgpack-1.2.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:dd3bfe82d53edfe4b7fc9a7ec9761e23a7a5b1dac22264505af428253c29ed24", size = 371959, upload-time = "2026-06-18T16:13:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/59/86/1edc67270099a528fa2093ea60fe191233cd238e4bd30cfacf7db79fc959/msgpack-1.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5ad5467fc3f68b5468e06c5f788d712e9f8ffc8b0cd1bcb160c105c1ee92dae7", size = 408457, upload-time = "2026-06-18T16:13:34.567Z" }, + { url = "https://files.pythonhosted.org/packages/82/90/8b630fef07d8c5ab457b71ff2c217910c83d333c7a68472c186e87cc504a/msgpack-1.2.1-cp314-cp314-win32.whl", hash = "sha256:98b58bdb89c46190e4609bb36abe17c6d4105ad13f9c5f8f6f64d320f8ced3fb", size = 65942, upload-time = "2026-06-18T16:13:36.056Z" }, + { url = "https://files.pythonhosted.org/packages/16/f1/467b81e98b24dd3885d7b1857728797b4ffc76a7a7483af4fb321a07de3c/msgpack-1.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:74847557e28ce71bd3c438a447ca90e4b507e997ddbdef8a12a7b283b86c156b", size = 72627, upload-time = "2026-06-18T16:13:37.079Z" }, + { url = "https://files.pythonhosted.org/packages/a7/1d/5d8c4c89985feb6acefb82a09e501c60392261856d2408d20bfe4f0360b1/msgpack-1.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:b50b727bd652bdc37d950336c848ef20ec54a4cafc38dce19b1cd86ad625d0f7", size = 66908, upload-time = "2026-06-18T16:13:38.23Z" }, + { url = "https://files.pythonhosted.org/packages/1b/02/ad2afb678b4de94496cd432b581759b756a92c1192d8c767edd6b132efdc/msgpack-1.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8d00f177ca88a77c1cf848d204a38f249751650b601cb6532acc68805d8a8273", size = 86000, upload-time = "2026-06-18T16:13:39.44Z" }, + { url = "https://files.pythonhosted.org/packages/54/74/0b797484013128837f3b1cbb6cea019277c4de4e377dc512b4d9a0f92940/msgpack-1.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5bb9c386f0a329c035ddbab4b72d1028bf9627add8dda41070288563d57ed1b1", size = 86544, upload-time = "2026-06-18T16:13:40.447Z" }, + { url = "https://files.pythonhosted.org/packages/a9/b4/b774d7eb95561739907fec675582f83203cf41c597a418c2589b4bfb8e9d/msgpack-1.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20466cca18c49c7292a8984bc15d65857b171e7264bdcb5f96baf8be238791fc", size = 427661, upload-time = "2026-06-18T16:13:41.574Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f9/3243191dc9937e00756c8bc1b0272fed8f23758e43df2a3b46f533e5090f/msgpack-1.2.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:196300e7e5d6e74d50f1607ab9c06c4a1484c383cd22defd727902591f7e8dde", size = 426375, upload-time = "2026-06-18T16:13:42.936Z" }, + { url = "https://files.pythonhosted.org/packages/23/c7/1693111db9944ba4ad4b67a1e788400d78a0b6af7a6523dc7e4e58f8274b/msgpack-1.2.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575957e79cd51903a4e8495a242442949641e08f1efd5197b43bebd3ea7682b4", size = 380495, upload-time = "2026-06-18T16:13:44.306Z" }, + { url = "https://files.pythonhosted.org/packages/3e/2b/92f86956a0c13e8662f7e2ad630c4eb4db07497b967589bd5245e018b2c1/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8c2ed1e48cc0f460bf3c7780e7137ff21a4e18433451916f2442c1b21036cd7d", size = 410897, upload-time = "2026-06-18T16:13:45.629Z" }, + { url = "https://files.pythonhosted.org/packages/da/ea/1479f72d200313a76fc2f823a79d1e07ed052ab7b8a0280640aa7b95de42/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5f6277e5f783c36786a145e0247fc189a03f35f84b251646e53592d2bc12b355", size = 378519, upload-time = "2026-06-18T16:13:46.998Z" }, + { url = "https://files.pythonhosted.org/packages/f5/4d/fa006060ffa1011d32bfae826fe766fe73e02982183601633b7121058ab3/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9389552ecf4784886345ead0647e4edc96bee37cbab05b75540f542f766c48c", size = 419815, upload-time = "2026-06-18T16:13:48.205Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/aab6c946570496b78e67804721f3d5e2d62a93081b9b37df77764ef56347/msgpack-1.2.1-cp314-cp314t-win32.whl", hash = "sha256:c1c79a604a2969a868a78b6ebd27a887e00c624f14f66b3038e0590cb23332d1", size = 70914, upload-time = "2026-06-18T16:13:49.385Z" }, + { url = "https://files.pythonhosted.org/packages/13/0a/e608956488a2af014cfe6e3d665e090b8ee42aa14b07f8f95b8880d66b09/msgpack-1.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f12038a35fabd52e56a3547bab42401af49a45caa6dd00b34c44de235bc93ee2", size = 77999, upload-time = "2026-06-18T16:13:50.467Z" }, + { url = "https://files.pythonhosted.org/packages/d2/8a/27e2e57055176e366a46b85d02d68e7a5bcfbdd8474c9706375d965f24d3/msgpack-1.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0adcf06ffde0777c0e1a9b771a2b1c4226ba1bbf748c8efcc02fcdeca3299107", size = 71160, upload-time = "2026-06-18T16:13:51.498Z" }, +] + +[[package]] +name = "narwhals" +version = "2.24.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/1d/58946e5aab18393e793bd4add6985b95d0e01c3a2d832f38f54468b10dcd/narwhals-2.24.0.tar.gz", hash = "sha256:b5c0f684ccd9d7475b564111e319a4964abcf2baf79d3cf6b1003d06ac9b828d", size = 661143, upload-time = "2026-07-13T10:49:19.086Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/85/a5bfaebfd305ac18b57b0854d74e37e586809061a91fda62f0bd50c8518e/narwhals-2.24.0-py3-none-any.whl", hash = "sha256:42fdedf44e5b2ca7505630d45b4ac3058f38d8485cba9fe1652ca23152df7489", size = 461030, upload-time = "2026-07-13T10:49:17.571Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "numba" +version = "0.66.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llvmlite" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/a0/570e3dc53e5602b49108f62a13e529f1eec8bfc7ef37d49c825924dcf546/numba-0.66.0.tar.gz", hash = "sha256:b900e63a0e26c05ea9a6d5a3a5a0a177cb64c5011887bf43edb8c3ed2c38d363", size = 2806181, upload-time = "2026-07-01T23:12:46.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/02/970796b4daa709604cde22e87a7cda9bde473c278ea4a75f59fe38cee47f/numba-0.66.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:bbd531c327557a9004507fa6bff06c53ab51a7a5776b75261bb9cef1efe2b2ea", size = 2727049, upload-time = "2026-07-01T23:12:11.296Z" }, + { url = "https://files.pythonhosted.org/packages/8c/99/33a6ed9c1a0b5e42efa98eb0edf617d61dca576c82625947377b1d4540c9/numba-0.66.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc6629becb21a867d85401ec89f426dd24c484a4193ade8a38309debfd1529ca", size = 3808870, upload-time = "2026-07-01T23:12:12.944Z" }, + { url = "https://files.pythonhosted.org/packages/04/20/8c51126025211659235b8de2866dfa226984ae0c8273461a3cf374716741/numba-0.66.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aac69f3ccb8af100f5913c1241edc9692bad1cdd2508721713f426eb06c9a659", size = 3514498, upload-time = "2026-07-01T23:12:15.307Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c9/9476940bc6d5caf5c0cf2e4c5feecbf01244bbe6f914614082dd7a3e520e/numba-0.66.0-cp311-cp311-win_amd64.whl", hash = "sha256:fb601841d9e02e6237bb6522e36d0741614be3cfe2b482a6f00a41b5ba209443", size = 2780225, upload-time = "2026-07-01T23:12:16.924Z" }, + { url = "https://files.pythonhosted.org/packages/62/a3/70deb7f88461c1cd5d16aa990c2380604102661a427667b8950dcdccc27f/numba-0.66.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:53ca5900b7cab15109796030113a6b28576bae5ad7bb507ad6dd1360ddd81ba4", size = 2727264, upload-time = "2026-07-01T23:12:18.669Z" }, + { url = "https://files.pythonhosted.org/packages/2d/55/25c319845e9a4e08f16611ddbda56a192eb7b6ed13e1a2bff2da272ffb97/numba-0.66.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0999e3ee1b18c48e1fb51d11af35ef59852c7f4f50569c9550c25faef0616ad1", size = 3866252, upload-time = "2026-07-01T23:12:20.429Z" }, + { url = "https://files.pythonhosted.org/packages/71/ef/a82d6fd6bf1b0fe461651e924d3647eeec9ac17f8eee4896264bf7480930/numba-0.66.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efe0d2d5099790df945e0cb6e1b3104bd965d7bbfac50d62f1d5d1d6ade0825d", size = 3566974, upload-time = "2026-07-01T23:12:22.116Z" }, + { url = "https://files.pythonhosted.org/packages/fc/eb/9e6171e378822ab191c7abcfd3d8cfc8644516f6c7834c22e210e4acc070/numba-0.66.0-cp312-cp312-win_amd64.whl", hash = "sha256:b075a4e7ebc43dc6294f223e2821659656209fd5e0ce53245877c23d66d6e1a9", size = 2797403, upload-time = "2026-07-01T23:12:23.724Z" }, + { url = "https://files.pythonhosted.org/packages/03/52/176c02d005c5c5143cde10a85bbcdcb6236d9e34c3aac089380e0506cd1d/numba-0.66.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:380b2556a2019ccd1e956ae77dd257eaa39403f7520768b626d44b755112785e", size = 2727084, upload-time = "2026-07-01T23:12:25.434Z" }, + { url = "https://files.pythonhosted.org/packages/44/b5/e930010965568fe7f2c6c962fd2849d458cb9f62c3ab7584af8a19a2b40a/numba-0.66.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:939316d5d8619751207b8972a67852b5a7646665298cb4de693cd6bf135152f4", size = 3873663, upload-time = "2026-07-01T23:12:27.308Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ec/5b51457cbe96e4831141d83e892e65191b23a1b78728456c62909d231ace/numba-0.66.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cdf506775d9f02eb92a87bf5c5b1e0d25506fd18cafd769f4ed914a8feac73e7", size = 3573529, upload-time = "2026-07-01T23:12:28.944Z" }, + { url = "https://files.pythonhosted.org/packages/83/7e/cea7710e96913d3c7f2999f16db1b28e6c5be5171cbf40f77f98333a7243/numba-0.66.0-cp313-cp313-win_amd64.whl", hash = "sha256:c5bfe5350284509ab0474390321454c3a8627a188af5b68c910e83df3e2db4a7", size = 2797247, upload-time = "2026-07-01T23:12:30.774Z" }, + { url = "https://files.pythonhosted.org/packages/96/7a/7e0e73550eb4e41ede6e72fb5371f4539537a4d770a3b73fa9b61aea0622/numba-0.66.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:46ae5f2b19e2af3c33c2df100306a90ea2f981c8158b0390f8bf6c20eee7357e", size = 2727296, upload-time = "2026-07-01T23:12:32.39Z" }, + { url = "https://files.pythonhosted.org/packages/0f/26/885774c006de6620ed3d10f45d8e20fe0b8e6aad6d573211a2cbc8b3e528/numba-0.66.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e2b101f23b8b63978d334574d2039f27f0dccfe1d891756f33a2e2f3e4c88cf4", size = 3842720, upload-time = "2026-07-01T23:12:33.938Z" }, + { url = "https://files.pythonhosted.org/packages/93/99/edebf7de890b73973d839dd971cf73734adfb81ffa1b4504f84b9059c3e5/numba-0.66.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:63b943eb2c9ba371908ce2cd6dfc643db51fc40f7966993376a1701bc922f537", size = 3543537, upload-time = "2026-07-01T23:12:35.566Z" }, + { url = "https://files.pythonhosted.org/packages/66/c5/b46ad28ac3681d035ea21365c5e052149062e1a0a9affd0563d2760ea6ff/numba-0.66.0-cp314-cp314-win_amd64.whl", hash = "sha256:bd57790acd20f6a468e0ad333ef6b82355e309a92310fb7dff80e919f01a21a9", size = 2799250, upload-time = "2026-07-01T23:12:37.154Z" }, + { url = "https://files.pythonhosted.org/packages/10/6f/5e77a7397a37dd16f57a7b72e7e470db5227b68e3639df0d13a8e674883d/numba-0.66.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:db7735d15ea17a283d6485b9fa3504769f78fd86e5146638ad5e8da57c031b9e", size = 2730342, upload-time = "2026-07-01T23:12:38.758Z" }, + { url = "https://files.pythonhosted.org/packages/39/fd/e9c9680a3813f3d781c20e5d53c1074801b787d4feecca0472fdd7c05ce1/numba-0.66.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:651a2b53298340956db26ecbe7ab043106b50a40c2807e66d617a4917245a4ab", size = 3878695, upload-time = "2026-07-01T23:12:40.302Z" }, + { url = "https://files.pythonhosted.org/packages/61/3a/9b363287b85fcd4537ea3878793822878b2ac1008a78159d2096fea628de/numba-0.66.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c1144ba1720ea59ad79f4f488ed54d149b2613b357f7e445678b7d0739c70e9", size = 3596323, upload-time = "2026-07-01T23:12:42.805Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f2/dca53d50b8f2289dd01954ace9da261e0487d5b74b188b4304e4ecc3492c/numba-0.66.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d426178fb991a85714c43112a8ea7b9d9579ea856ad8dcdb9c1c3941903ba5be", size = 2804772, upload-time = "2026-07-01T23:12:44.399Z" }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, +] + +[[package]] +name = "nvidia-cublas" +version = "13.1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cuda-nvrtc" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" }, + { url = "https://files.pythonhosted.org/packages/3b/cd/154ca20c38269e05eff77c1464e6c1da89f50a6390b565e9d82e06bc11e1/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:37936a16db8fe4ac1f065c2139360608a543a09275cb1a1af612e08cfa065436", size = 423138758, upload-time = "2026-04-08T18:46:58.655Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/2a/80353b103fc20ce05ef51e928daed4b6015db4aaa9162ed0997090fe2250/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151", size = 10310827, upload-time = "2025-09-04T08:26:42.012Z" }, + { url = "https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8", size = 10715597, upload-time = "2025-09-04T08:26:51.312Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575", size = 90215200, upload-time = "2025-09-04T08:28:44.204Z" }, + { url = "https://files.pythonhosted.org/packages/b7/dc/6bb80850e0b7edd6588d560758f17e0550893a1feaf436807d64d2da040f/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b", size = 43015449, upload-time = "2025-09-04T08:28:20.239Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime" +version = "13.0.96" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/4f/17d7b9b8e285199c58ce28e31b5c5bbaa4d8271af06a89b6405258245de2/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55", size = 2261060, upload-time = "2025-10-09T08:55:15.78Z" }, + { url = "https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548", size = 2243632, upload-time = "2025-10-09T08:55:36.117Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu13" +version = "9.20.0.48" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/c5/83384d846b2fd17c44bd499b36c75a45ed4f095fbbb2252294e89cea5c5c/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:e31454ae00094b0c55319d9d15b6fa2fc50a9e1c0f5c8c80fb75258234e731e1", size = 444574296, upload-time = "2026-03-09T19:28:27.751Z" }, + { url = "https://files.pythonhosted.org/packages/6e/5e/edb9c0ae051602c3ccaffe424256463636d639e27d7f302dde9975ef9e7a/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0c45dd8eeb50b603f07995b1b300c62ffe6a1980482b82b3bcf94a4ca9d49304", size = 366173588, upload-time = "2026-03-09T19:29:34.474Z" }, +] + +[[package]] +name = "nvidia-cufft" +version = "12.0.0.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3", size = 214085489, upload-time = "2025-09-04T08:31:56.044Z" }, +] + +[[package]] +name = "nvidia-cufile" +version = "1.15.1.6" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44", size = 1223672, upload-time = "2025-09-04T08:32:22.779Z" }, + { url = "https://files.pythonhosted.org/packages/ab/73/cc4a14c9813a8a0d509417cf5f4bdaba76e924d58beb9864f5a7baceefbf/nvidia_cufile-1.15.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1", size = 1136992, upload-time = "2025-09-04T08:32:14.119Z" }, +] + +[[package]] +name = "nvidia-curand" +version = "10.4.0.35" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/72/7c2ae24fb6b63a32e6ae5d241cc65263ea18d08802aaae087d9f013335a2/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a", size = 61962106, upload-time = "2025-08-04T10:21:41.128Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc", size = 59544258, upload-time = "2025-08-04T10:22:03.992Z" }, +] + +[[package]] +name = "nvidia-cusolver" +version = "12.0.4.66" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas" }, + { name = "nvidia-cusparse" }, + { name = "nvidia-nvjitlink" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, + { url = "https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112", size = 200941980, upload-time = "2025-09-04T08:33:22.767Z" }, +] + +[[package]] +name = "nvidia-cusparse" +version = "12.6.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, + { url = "https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b", size = 145942937, upload-time = "2025-09-04T08:33:58.029Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu13" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/e1/cdc1797eadf82d3a9a575a19b33fdc871a97edbec42c00b5b5e914f4aff4/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4dca476c50bf4780d46cd0bfbd82e2bc10a08e4fef7950917ce8d7578d22a23f", size = 221051344, upload-time = "2025-09-05T18:49:51.289Z" }, + { url = "https://files.pythonhosted.org/packages/34/7d/2661f2fb3ac4302f3a246f5fc030213ac60c1fe0bce84f9783dbd831dbb7/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:786ce87568c303fadb5afcc7102d454cd3040d75f6f8626f5db460d1871f4dd0", size = 170148586, upload-time = "2025-09-05T18:50:50.248Z" }, +] + +[[package]] +name = "nvidia-nccl-cu13" +version = "2.29.7" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/0d/daf50d44177ee0cbc7ff0a0c91eb5ff676c82be42f9a970bc7597f440c3a/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:674a12383e3c38a1bcccae7d4f3633b37852230b6047883cb2f4c2d1b36d9bf5", size = 206014712, upload-time = "2026-03-03T05:34:20.843Z" }, + { url = "https://files.pythonhosted.org/packages/67/f4/58e4e91b6919367c7aafb8e36fce9aad1a3047e536bf7e2fd560927d3a4c/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:edd81538446786ec3b73972543e53bb43bcaf0bfc8ef76cb679fcc390ffe136d", size = 205976000, upload-time = "2026-03-03T05:36:24.472Z" }, +] + +[[package]] +name = "nvidia-nvjitlink" +version = "13.3.33" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/ee/580ca6f29dcab0221db8706badca1bbbb084f1975c4d4e83329c3a7e31f0/nvidia_nvjitlink-13.3.33-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:26a6de7fb4c8fdaa7703d3dad720d6d427ddfea5c48a528fd97c11733ad830e5", size = 40742423, upload-time = "2026-05-26T16:54:51.613Z" }, + { url = "https://files.pythonhosted.org/packages/69/30/45414e35ff2eee7db3da037e5707037ccf9d2b5218ffbdb055ea4d5aa98a/nvidia_nvjitlink-13.3.33-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ce48b37dfeb3cb1eae4cf85adacb47d7a6539ea2272870c9a3628ce275c2037e", size = 39168635, upload-time = "2026-05-26T16:54:13.906Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu13" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/0f/05cc9c720236dcd2db9c1ab97fff629e96821be2e63103569da0c9b72f19/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9", size = 60215947, upload-time = "2025-09-06T00:32:20.022Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80", size = 60412546, upload-time = "2025-09-06T00:32:41.564Z" }, +] + +[[package]] +name = "nvidia-nvtx" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4", size = 148047, upload-time = "2025-09-04T08:29:01.761Z" }, + { url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" }, +] + +[[package]] +name = "omegaconf" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "antlr4-python3-runtime" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/3d/e4b57b8d9008c6ebe0d5eff901f91d5700cf7bdb8c8863df817463a7fd5e/omegaconf-2.3.1.tar.gz", hash = "sha256:e5e7de64aeebeddaf8e6d3f7a783b32ac2a01c0fbd9c878012caecb891a1f42a", size = 3298472, upload-time = "2026-06-11T05:05:12.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/0e/152509871bf30df6fc38569f52a2db9b55dd41aae957adae50a053ac7778/omegaconf-2.3.1-py3-none-any.whl", hash = "sha256:3d701d14e9a8828f1edd28bb70b725908b34277cdd72cf7d6a83f94dadc6b6a0", size = 79502, upload-time = "2026-06-11T05:05:09.954Z" }, +] + +[[package]] +name = "openunmix" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "torch" }, + { name = "torchaudio" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/ef/4ad54e3ecb1e89f7f7bdb4c7b751e43754e892d3c32a8550e5d0882565df/openunmix-1.3.0.tar.gz", hash = "sha256:cc9245ce728700f5d0b72c67f01be4162777e617cdc47f9b035963afac180fc8", size = 45889, upload-time = "2024-04-16T11:10:47.121Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/37/320afd9458abb186f09a5183f36e48829df7151821bf887f272a63b2584d/openunmix-1.3.0-py3-none-any.whl", hash = "sha256:e893ae22c5b8001a6107022499c2587b70d5c2e4777cc7c9ed6272b68a69534e", size = 40047, upload-time = "2024-04-16T11:10:45.107Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/9b/560e4be8e26f6fd133a03630a8df0c663b9e8d61b4ade152b72005aec83b/platformdirs-4.11.0.tar.gz", hash = "sha256:0555d18370482847566ffabcaa53ad7c6c1c29f195989ae1ed634a05f76ea1e0", size = 31953, upload-time = "2026-07-21T13:09:36.565Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/68/d8d58938dfb1370b266a1a729e6d77a985be23689a0496498ee17b2cbf90/platformdirs-4.11.0-py3-none-any.whl", hash = "sha256:360ccded2b7fce0af0ff80cc8f5942a1c5d99b0e856033acb030bfc634709e74", size = 23247, upload-time = "2026-07-21T13:09:35.422Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pooch" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "platformdirs" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/43/85ef45e8b36c6a48546af7b266592dc32d7f67837a6514d111bced6d7d75/pooch-1.9.0.tar.gz", hash = "sha256:de46729579b9857ffd3e741987a2f6d5e0e03219892c167c6578c0091fb511ed", size = 61788, upload-time = "2026-01-30T19:15:09.649Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl", hash = "sha256:f265597baa9f760d25ceb29d0beb8186c243d6607b0f60b83ecf14078dbc703b", size = 67175, upload-time = "2026-01-30T19:15:08.36Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.11.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/86/8ce9040065e8f924d642c58e4a344e33163a07f6b57f836d0d734e0ad3fb/pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a", size = 787102, upload-time = "2025-05-22T21:18:08.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7", size = 444229, upload-time = "2025-05-22T21:18:06.329Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584, upload-time = "2025-04-23T18:31:03.106Z" }, + { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071, upload-time = "2025-04-23T18:31:04.621Z" }, + { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823, upload-time = "2025-04-23T18:31:06.377Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792, upload-time = "2025-04-23T18:31:07.93Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338, upload-time = "2025-04-23T18:31:09.283Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998, upload-time = "2025-04-23T18:31:11.7Z" }, + { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200, upload-time = "2025-04-23T18:31:13.536Z" }, + { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890, upload-time = "2025-04-23T18:31:15.011Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359, upload-time = "2025-04-23T18:31:16.393Z" }, + { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883, upload-time = "2025-04-23T18:31:17.892Z" }, + { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074, upload-time = "2025-04-23T18:31:19.205Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538, upload-time = "2025-04-23T18:31:20.541Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909, upload-time = "2025-04-23T18:31:22.371Z" }, + { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786, upload-time = "2025-04-23T18:31:24.161Z" }, + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, + { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200, upload-time = "2025-04-23T18:33:14.199Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123, upload-time = "2025-04-23T18:33:16.555Z" }, + { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852, upload-time = "2025-04-23T18:33:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484, upload-time = "2025-04-23T18:33:20.475Z" }, + { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896, upload-time = "2025-04-23T18:33:22.501Z" }, + { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475, upload-time = "2025-04-23T18:33:24.528Z" }, + { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013, upload-time = "2025-04-23T18:33:26.621Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715, upload-time = "2025-04-23T18:33:28.656Z" }, + { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234, upload-time = "2025-04-18T16:44:48.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356, upload-time = "2025-04-18T16:44:46.617Z" }, +] + +[[package]] +name = "pyproject-hooks" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, +] + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "0.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/c4/453c52c659521066969523e87d85d54139bbd17b78f09532fb8eb8cdb58e/pytest_asyncio-0.26.0.tar.gz", hash = "sha256:c4df2a697648241ff39e7f0e4a73050b03f123f760673956cf0d72a4990e312f", size = 54156, upload-time = "2025-03-25T06:22:28.883Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/7f/338843f449ace853647ace35870874f69a764d251872ed1b4de9f234822c/pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0", size = 19694, upload-time = "2025-03-25T06:22:27.807Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920, upload-time = "2025-03-25T10:14:56.835Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256, upload-time = "2025-03-25T10:14:55.034Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "retrying" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c8/5a/b17e1e257d3e6f2e7758930e1256832c9ddd576f8631781e6a072914befa/retrying-1.4.2.tar.gz", hash = "sha256:d102e75d53d8d30b88562d45361d6c6c934da06fab31bd81c0420acb97a8ba39", size = 11411, upload-time = "2025-08-03T03:35:25.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/f3/6cd296376653270ac1b423bb30bd70942d9916b6978c6f40472d6ac038e7/retrying-1.4.2-py3-none-any.whl", hash = "sha256:bbc004aeb542a74f3569aeddf42a2516efefcdaff90df0eb38fbfbf19f179f59", size = 10859, upload-time = "2025-08-03T03:35:23.829Z" }, +] + +[[package]] +name = "ruff" +version = "0.11.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/53/ae4857030d59286924a8bdb30d213d6ff22d8f0957e738d0289990091dd8/ruff-0.11.11.tar.gz", hash = "sha256:7774173cc7c1980e6bf67569ebb7085989a78a103922fb83ef3dfe230cd0687d", size = 4186707, upload-time = "2025-05-22T19:19:34.363Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/14/f2326676197bab099e2a24473158c21656fbf6a207c65f596ae15acb32b9/ruff-0.11.11-py3-none-linux_armv6l.whl", hash = "sha256:9924e5ae54125ed8958a4f7de320dab7380f6e9fa3195e3dc3b137c6842a0092", size = 10229049, upload-time = "2025-05-22T19:18:45.516Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f3/bff7c92dd66c959e711688b2e0768e486bbca46b2f35ac319bb6cce04447/ruff-0.11.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c8a93276393d91e952f790148eb226658dd275cddfde96c6ca304873f11d2ae4", size = 11053601, upload-time = "2025-05-22T19:18:49.269Z" }, + { url = "https://files.pythonhosted.org/packages/e2/38/8e1a3efd0ef9d8259346f986b77de0f62c7a5ff4a76563b6b39b68f793b9/ruff-0.11.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d6e333dbe2e6ae84cdedefa943dfd6434753ad321764fd937eef9d6b62022bcd", size = 10367421, upload-time = "2025-05-22T19:18:51.754Z" }, + { url = "https://files.pythonhosted.org/packages/b4/50/557ad9dd4fb9d0bf524ec83a090a3932d284d1a8b48b5906b13b72800e5f/ruff-0.11.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7885d9a5e4c77b24e8c88aba8c80be9255fa22ab326019dac2356cff42089fc6", size = 10581980, upload-time = "2025-05-22T19:18:54.011Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b2/e2ed82d6e2739ece94f1bdbbd1d81b712d3cdaf69f0a1d1f1a116b33f9ad/ruff-0.11.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b5ab797fcc09121ed82e9b12b6f27e34859e4227080a42d090881be888755d4", size = 10089241, upload-time = "2025-05-22T19:18:56.041Z" }, + { url = "https://files.pythonhosted.org/packages/3d/9f/b4539f037a5302c450d7c695c82f80e98e48d0d667ecc250e6bdeb49b5c3/ruff-0.11.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e231ff3132c1119ece836487a02785f099a43992b95c2f62847d29bace3c75ac", size = 11699398, upload-time = "2025-05-22T19:18:58.248Z" }, + { url = "https://files.pythonhosted.org/packages/61/fb/32e029d2c0b17df65e6eaa5ce7aea5fbeaed22dddd9fcfbbf5fe37c6e44e/ruff-0.11.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a97c9babe1d4081037a90289986925726b802d180cca784ac8da2bbbc335f709", size = 12427955, upload-time = "2025-05-22T19:19:00.981Z" }, + { url = "https://files.pythonhosted.org/packages/6e/e3/160488dbb11f18c8121cfd588e38095ba779ae208292765972f7732bfd95/ruff-0.11.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8c4ddcbe8a19f59f57fd814b8b117d4fcea9bee7c0492e6cf5fdc22cfa563c8", size = 12069803, upload-time = "2025-05-22T19:19:03.258Z" }, + { url = "https://files.pythonhosted.org/packages/ff/16/3b006a875f84b3d0bff24bef26b8b3591454903f6f754b3f0a318589dcc3/ruff-0.11.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6224076c344a7694c6fbbb70d4f2a7b730f6d47d2a9dc1e7f9d9bb583faf390b", size = 11242630, upload-time = "2025-05-22T19:19:05.871Z" }, + { url = "https://files.pythonhosted.org/packages/65/0d/0338bb8ac0b97175c2d533e9c8cdc127166de7eb16d028a43c5ab9e75abd/ruff-0.11.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:882821fcdf7ae8db7a951df1903d9cb032bbe838852e5fc3c2b6c3ab54e39875", size = 11507310, upload-time = "2025-05-22T19:19:08.584Z" }, + { url = "https://files.pythonhosted.org/packages/6f/bf/d7130eb26174ce9b02348b9f86d5874eafbf9f68e5152e15e8e0a392e4a3/ruff-0.11.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:dcec2d50756463d9df075a26a85a6affbc1b0148873da3997286caf1ce03cae1", size = 10441144, upload-time = "2025-05-22T19:19:13.621Z" }, + { url = "https://files.pythonhosted.org/packages/b3/f3/4be2453b258c092ff7b1761987cf0749e70ca1340cd1bfb4def08a70e8d8/ruff-0.11.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:99c28505ecbaeb6594701a74e395b187ee083ee26478c1a795d35084d53ebd81", size = 10081987, upload-time = "2025-05-22T19:19:15.821Z" }, + { url = "https://files.pythonhosted.org/packages/6c/6e/dfa4d2030c5b5c13db158219f2ec67bf333e8a7748dccf34cfa2a6ab9ebc/ruff-0.11.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9263f9e5aa4ff1dec765e99810f1cc53f0c868c5329b69f13845f699fe74f639", size = 11073922, upload-time = "2025-05-22T19:19:18.104Z" }, + { url = "https://files.pythonhosted.org/packages/ff/f4/f7b0b0c3d32b593a20ed8010fa2c1a01f2ce91e79dda6119fcc51d26c67b/ruff-0.11.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:64ac6f885e3ecb2fdbb71de2701d4e34526651f1e8503af8fb30d4915a3fe345", size = 11568537, upload-time = "2025-05-22T19:19:20.889Z" }, + { url = "https://files.pythonhosted.org/packages/d2/46/0e892064d0adc18bcc81deed9aaa9942a27fd2cd9b1b7791111ce468c25f/ruff-0.11.11-py3-none-win32.whl", hash = "sha256:1adcb9a18802268aaa891ffb67b1c94cd70578f126637118e8099b8e4adcf112", size = 10536492, upload-time = "2025-05-22T19:19:23.642Z" }, + { url = "https://files.pythonhosted.org/packages/1b/d9/232e79459850b9f327e9f1dc9c047a2a38a6f9689e1ec30024841fc4416c/ruff-0.11.11-py3-none-win_amd64.whl", hash = "sha256:748b4bb245f11e91a04a4ff0f96e386711df0a30412b9fe0c74d5bdc0e4a531f", size = 11612562, upload-time = "2025-05-22T19:19:27.013Z" }, + { url = "https://files.pythonhosted.org/packages/ce/eb/09c132cff3cc30b2e7244191dcce69437352d6d6709c0adf374f3e6f476e/ruff-0.11.11-py3-none-win_arm64.whl", hash = "sha256:6c51f136c0364ab1b774767aa8b86331bd8e9d414e2d107db7a2189f35ea1f7b", size = 10735951, upload-time = "2025-05-22T19:19:30.043Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "narwhals" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fa/6f/37092bdb25f712817231799fc5674d8e704066a8a70c1d2d40517e18b4ab/scikit_learn-1.9.0.tar.gz", hash = "sha256:8833266989d3a5110178a9fae30783675460724d0e1efb13b14901d2c660c557", size = 7750767, upload-time = "2026-06-02T11:54:32.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/be/e844fd9586e66540a15b71924d17a6cbc1bb749e81ddd0a796bcdba4c055/scikit_learn-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9db6f4d34e68c8899e4cab27fdf8eafe6ed21f2ba52ceb25ea250cd237f8e47b", size = 8789686, upload-time = "2026-06-02T11:53:05.439Z" }, + { url = "https://files.pythonhosted.org/packages/42/e2/ff880f62677a17d035817d543cb0fc8727d01eccbee81c5f7fc733a9d856/scikit_learn-1.9.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f401448645a3e7bc115aa3c094097865155b34bff1cba8101857d9104e99074c", size = 8256782, upload-time = "2026-06-02T11:53:08.904Z" }, + { url = "https://files.pythonhosted.org/packages/25/64/eb40435e1a508ab1b4e284ce43ae80f6a162e5be5e38ed5a6fab467a9ea4/scikit_learn-1.9.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd3a8ef0c758555a3b23c03adaa858af32f7736785ded50ad5991f59c4ed03fa", size = 8992419, upload-time = "2026-06-02T11:53:11.551Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/4810a28e473185429e45a57eebcc91fc991b33d889cc0676063e671db03d/scikit_learn-1.9.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7e254636164090da847715a27f8e5478feb98c40a9e0ee90cbd277de9e5ceb8", size = 9281411, upload-time = "2026-06-02T11:53:15.063Z" }, + { url = "https://files.pythonhosted.org/packages/3b/67/be3d369f40d8178ba3bd86635d132e08cb5329b023e4669d9426d84bc007/scikit_learn-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:5dc1818c77575d149e25fce9ef82dd7b7263ae372f03494158668ad632a69759", size = 8272736, upload-time = "2026-06-02T11:53:18.108Z" }, + { url = "https://files.pythonhosted.org/packages/37/79/a733f02dc2118da7e77a134b34f39f40201a353311b011d20859d2db3556/scikit_learn-1.9.0-cp311-cp311-win_arm64.whl", hash = "sha256:366652351f092b219c248f1e72821e841960a63d8f358f1dcfd54dc1cbdbbc28", size = 7919564, upload-time = "2026-06-02T11:53:21.2Z" }, + { url = "https://files.pythonhosted.org/packages/ac/20/75f915ff375d6249e6550ac740fdbbd66159a068fd3af1400ff62036b07a/scikit_learn-1.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2bd41b0d201bc81575531b96b713d3eb5e5f50fb0b82101ff0f92294fdc236ac", size = 8741122, upload-time = "2026-06-02T11:53:24.08Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d5/2b5148f2279196775e1db2aeb85d14b70ac80e7e32b3b28e7ebeafb0901d/scikit_learn-1.9.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5be45aa4a42a68a533913a6ed736cf309de2226411c79ef8d609a5456f1939b1", size = 8261512, upload-time = "2026-06-02T11:53:27.183Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ee/5adbc77656b71f9456a2f5a7a9fdb4bcf9207a6b962889f1c2f9323afa4e/scikit_learn-1.9.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e50ed4da51974e86e940690e9a3d82e729b62b5a49f7c9bac534d515d39d86f", size = 8837603, upload-time = "2026-06-02T11:53:30.328Z" }, + { url = "https://files.pythonhosted.org/packages/6c/c2/63fdda36c56437eeb44aaf9493c8bcd62ce230ab1598924fc626ffbfa943/scikit_learn-1.9.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:056c92bb67ad4c28463c2f2653d9701449201e7e7a9e94e321be0f71c4fef2b8", size = 9132097, upload-time = "2026-06-02T11:53:33.456Z" }, + { url = "https://files.pythonhosted.org/packages/83/a4/c8e67227c680e2259c8864ae72ff48b06e16a6f51253a22167aa02a8aa4e/scikit_learn-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:4306775fad04cc4b472a1b15af1ae9cede1540fbfcc17fbce3767cd8dc7ae283", size = 8211173, upload-time = "2026-06-02T11:53:36.602Z" }, + { url = "https://files.pythonhosted.org/packages/cf/fd/3c0863792e98e67e9184aa4029288a175935eb65443afcd30d4f143450cf/scikit_learn-1.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:26e22435f63bcdcf396b574273f29f13dd531f5ea035801f5be10ba1540a4e60", size = 7867451, upload-time = "2026-06-02T11:53:39.075Z" }, + { url = "https://files.pythonhosted.org/packages/3c/01/cf3310626b6d48d3e9be69a1223f9180360b5e6edb045f50fade723ce494/scikit_learn-1.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:80746d63bd4b6eaca54d36fe5feaf4d28bb38dc6f9470f81c7cad7c40155f119", size = 8705188, upload-time = "2026-06-02T11:53:41.964Z" }, + { url = "https://files.pythonhosted.org/packages/3e/04/5acd7ae280c5f93b6ac5ef6cdec14eef4c8d1cd91d85b3292989c94d96b1/scikit_learn-1.9.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5b934c45c252844a91d69fda3a34cff5e7307e1db10d77cb10a3980312c74713", size = 8228299, upload-time = "2026-06-02T11:53:44.817Z" }, + { url = "https://files.pythonhosted.org/packages/0c/39/ffe829a5b8ecb40a518724a997794657fdc354ada5e8fe8e64d998c0bac9/scikit_learn-1.9.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:38c3dcb9a1ffb85505ec53d54c7b4aea0cff70050425a7760c2af661ac85df05", size = 8789690, upload-time = "2026-06-02T11:53:47.461Z" }, + { url = "https://files.pythonhosted.org/packages/1f/88/8dab5de10c638c083772a6be83a3d8106ced492f74a928c8693638e5bb50/scikit_learn-1.9.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da76d09304a4706db7cc1e3ebaa3b6b98a67365cc11d2996c4f1e58ba47df714", size = 9087723, upload-time = "2026-06-02T11:53:50.702Z" }, + { url = "https://files.pythonhosted.org/packages/20/3f/7917ca72464038f6240ec70c29f94862d08a34a74291ae4d4ec5eb8186a0/scikit_learn-1.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:5808d98f15c6bf6d9d96d2348c1997392a5888ce7097e664105f930c4bca1277", size = 8184330, upload-time = "2026-06-02T11:53:53.396Z" }, + { url = "https://files.pythonhosted.org/packages/78/c7/15739eb2f61fda3c54639e9942414e5a19ad8a8d1f5a3266afad7cb7df80/scikit_learn-1.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:d77f54c017633791bc0225a43e2f8d03745fdcfe4880268fcc4df15f505dec2e", size = 7840653, upload-time = "2026-06-02T11:53:56.035Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7d/c9a35cf59b20a86fec24d306f1547b78dec194b08d367ce2a3e4854169d9/scikit_learn-1.9.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9656acd4e93f74e0b66c8a36c88830a99252dfa900044d36bc2212ae89a47162", size = 8713289, upload-time = "2026-06-02T11:53:58.788Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a7/552a7821597c632b907f7bfe8f36f9f572777af8ef8a48353041cf8e091a/scikit_learn-1.9.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:24360002ae845e7866522b0a5bbf690802e7bc388cac8663502e78aa98598aa2", size = 8245141, upload-time = "2026-06-02T11:54:01.694Z" }, + { url = "https://files.pythonhosted.org/packages/7d/79/f4a0c4fe9711154cddabf913471153af79056382ddc612cfe5ee0ff4b72e/scikit_learn-1.9.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5162ad10a418c8a282dde04c9aa06965de3e9a65f33c1440c0ae69bb1a09d913", size = 8847671, upload-time = "2026-06-02T11:54:04.448Z" }, + { url = "https://files.pythonhosted.org/packages/f0/af/4d72d9e475ac83719160c662619e4bf7b95c19507cd582e7d0167a3c3dae/scikit_learn-1.9.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fea2cc5677ab49d6f5bade978c866da44957b712d92e9635e8b4f723013c3cb", size = 9118104, upload-time = "2026-06-02T11:54:07.205Z" }, + { url = "https://files.pythonhosted.org/packages/a2/d5/6a58eea2cb9abbb9b3f2bb8b2cfb3243d1152d69f442d256c7af71304769/scikit_learn-1.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:64fa347efc1c839c487433e40c5144d38c336e8a2b59c81aa8660373945c2673", size = 8290674, upload-time = "2026-06-02T11:54:10.087Z" }, + { url = "https://files.pythonhosted.org/packages/65/5b/d4c879cf358f1187141cf90ced473f087183489090244f50c124a2ee478b/scikit_learn-1.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:1b944b6db288f6b926e3650026ddafb988929de95d11fc2cc5fa117773c9ba42", size = 7978807, upload-time = "2026-06-02T11:54:12.769Z" }, + { url = "https://files.pythonhosted.org/packages/8a/43/bfae3121ec67ae09150d453c442c7c1cc166e9aefe056e6ab3b7728a5cfc/scikit_learn-1.9.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4ccacf04ca5f4b492158a5f28afe0ace43f81b2571e4b9a66d34848b46128949", size = 9031941, upload-time = "2026-06-02T11:54:15.436Z" }, + { url = "https://files.pythonhosted.org/packages/75/b0/20a4546eb17f3b25d3c66df15810411c14ed5065bcfab50b53c96fb627b2/scikit_learn-1.9.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:ee1a8db2c18c08e34c7412d4b10be1cac214cd4ea7dc9715a6a327eb49a37c96", size = 8613528, upload-time = "2026-06-02T11:54:18.842Z" }, + { url = "https://files.pythonhosted.org/packages/18/3c/e440e039bb82cd19004edaaad00acbde0fb9b461083c3ecf37941c557312/scikit_learn-1.9.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:147e9329ef0e39f75d4cffa02b2aa48d827832684926cd5210d9a2cb5c57246b", size = 8855050, upload-time = "2026-06-02T11:54:21.699Z" }, + { url = "https://files.pythonhosted.org/packages/43/26/b341b8dab5998da6270a3a42c2152c578501354d36f944b5856757035ef8/scikit_learn-1.9.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bad8f8b9950321b54c965fdcbac6c6c55e79e16646b49977bcf3668d3870a1a", size = 9097190, upload-time = "2026-06-02T11:54:24.454Z" }, + { url = "https://files.pythonhosted.org/packages/fb/de/b650b4d69b84468cfa2e28a3ff7b8103743029e6446ce1a97fe060ef688c/scikit_learn-1.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:78fc56eafd4edb9575d2d8950d1dd152061abb573341a1cb7e099fc40f6c6666", size = 8963204, upload-time = "2026-06-02T11:54:27.428Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f3/ff83d76d7418112e5a61326443cdda87be3545dd8d6599c95b2481a4419e/scikit_learn-1.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:051075bda8b7aab87b1906ab3d4740a1e1224a19d7b3781a576736edc94e76aa", size = 8222661, upload-time = "2026-06-02T11:54:30.192Z" }, +] + +[[package]] +name = "scipy" +version = "1.15.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +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 = [ + { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255, upload-time = "2025-05-08T16:05:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035, upload-time = "2025-05-08T16:05:20.152Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499, upload-time = "2025-05-08T16:05:24.494Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602, upload-time = "2025-05-08T16:05:29.313Z" }, + { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415, upload-time = "2025-05-08T16:05:34.699Z" }, + { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622, upload-time = "2025-05-08T16:05:40.762Z" }, + { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796, upload-time = "2025-05-08T16:05:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684, upload-time = "2025-05-08T16:05:54.22Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504, upload-time = "2025-05-08T16:06:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735, upload-time = "2025-05-08T16:06:06.471Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284, upload-time = "2025-05-08T16:06:11.686Z" }, + { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958, upload-time = "2025-05-08T16:06:15.97Z" }, + { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454, upload-time = "2025-05-08T16:06:20.394Z" }, + { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199, upload-time = "2025-05-08T16:06:26.159Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455, upload-time = "2025-05-08T16:06:32.778Z" }, + { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140, upload-time = "2025-05-08T16:06:39.249Z" }, + { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload-time = "2025-05-08T16:06:45.729Z" }, + { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload-time = "2025-05-08T16:06:52.623Z" }, + { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256, upload-time = "2025-05-08T16:06:58.696Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540, upload-time = "2025-05-08T16:07:04.209Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115, upload-time = "2025-05-08T16:07:08.998Z" }, + { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884, upload-time = "2025-05-08T16:07:14.091Z" }, + { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018, upload-time = "2025-05-08T16:07:19.427Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716, upload-time = "2025-05-08T16:07:25.712Z" }, + { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342, upload-time = "2025-05-08T16:07:31.468Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869, upload-time = "2025-05-08T16:07:38.002Z" }, + { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851, upload-time = "2025-05-08T16:08:33.671Z" }, + { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011, upload-time = "2025-05-08T16:07:44.039Z" }, + { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407, upload-time = "2025-05-08T16:07:49.891Z" }, + { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030, upload-time = "2025-05-08T16:07:54.121Z" }, + { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709, upload-time = "2025-05-08T16:07:58.506Z" }, + { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045, upload-time = "2025-05-08T16:08:03.929Z" }, + { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062, upload-time = "2025-05-08T16:08:09.558Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132, upload-time = "2025-05-08T16:08:15.34Z" }, + { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503, upload-time = "2025-05-08T16:08:21.513Z" }, + { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097, upload-time = "2025-05-08T16:08:27.627Z" }, +] + +[[package]] +name = "setuptools" +version = "83.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/34/26/f5d29e25ffdb535afef2d35cdb55b325298f96debd670da4c325e08d70f4/setuptools-83.0.0.tar.gz", hash = "sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef", size = 1154254, upload-time = "2026-07-04T15:31:22.699Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/40/e1e72872c6354b306daef1703549e8e83b4d43cfea356311bf722a043752/setuptools-83.0.0-py3-none-any.whl", hash = "sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3", size = 1008090, upload-time = "2026-07-04T15:31:20.885Z" }, +] + +[[package]] +name = "soundfile" +version = "0.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/41/9b873a8c055582859b239be17902a85339bec6a30ad162f98c9b0288a2cc/soundfile-0.13.1.tar.gz", hash = "sha256:b2c68dab1e30297317080a5b43df57e302584c49e2942defdde0acccc53f0e5b", size = 46156, upload-time = "2025-01-25T09:17:04.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/28/e2a36573ccbcf3d57c00626a21fe51989380636e821b341d36ccca0c1c3a/soundfile-0.13.1-py2.py3-none-any.whl", hash = "sha256:a23c717560da2cf4c7b5ae1142514e0fd82d6bbd9dfc93a50423447142f2c445", size = 25751, upload-time = "2025-01-25T09:16:44.235Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ab/73e97a5b3cc46bba7ff8650a1504348fa1863a6f9d57d7001c6b67c5f20e/soundfile-0.13.1-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:82dc664d19831933fe59adad199bf3945ad06d84bc111a5b4c0d3089a5b9ec33", size = 1142250, upload-time = "2025-01-25T09:16:47.583Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e5/58fd1a8d7b26fc113af244f966ee3aecf03cb9293cb935daaddc1e455e18/soundfile-0.13.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:743f12c12c4054921e15736c6be09ac26b3b3d603aef6fd69f9dde68748f2593", size = 1101406, upload-time = "2025-01-25T09:16:49.662Z" }, + { url = "https://files.pythonhosted.org/packages/58/ae/c0e4a53d77cf6e9a04179535766b3321b0b9ced5f70522e4caf9329f0046/soundfile-0.13.1-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:9c9e855f5a4d06ce4213f31918653ab7de0c5a8d8107cd2427e44b42df547deb", size = 1235729, upload-time = "2025-01-25T09:16:53.018Z" }, + { url = "https://files.pythonhosted.org/packages/57/5e/70bdd9579b35003a489fc850b5047beeda26328053ebadc1fb60f320f7db/soundfile-0.13.1-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:03267c4e493315294834a0870f31dbb3b28a95561b80b134f0bd3cf2d5f0e618", size = 1313646, upload-time = "2025-01-25T09:16:54.872Z" }, + { url = "https://files.pythonhosted.org/packages/fe/df/8c11dc4dfceda14e3003bb81a0d0edcaaf0796dd7b4f826ea3e532146bba/soundfile-0.13.1-py2.py3-none-win32.whl", hash = "sha256:c734564fab7c5ddf8e9be5bf70bab68042cd17e9c214c06e365e20d64f9a69d5", size = 899881, upload-time = "2025-01-25T09:16:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/14/e9/6b761de83277f2f02ded7e7ea6f07828ec78e4b229b80e4ca55dd205b9dc/soundfile-0.13.1-py2.py3-none-win_amd64.whl", hash = "sha256:1e70a05a0626524a69e9f0f4dd2ec174b4e9567f4d8b6c11d38b5c289be36ee9", size = 1019162, upload-time = "2025-01-25T09:16:59.573Z" }, +] + +[[package]] +name = "soxr" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/11/27cebce4a108f77afea7c80545115536b45e3f11ebfb914f638fdd9ba847/soxr-1.1.0.tar.gz", hash = "sha256:9f228ae21c78fa9359ca98d8a5e8e91f30639e438e574133dace62c5b5309e44", size = 173067, upload-time = "2026-05-03T00:15:18.214Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/49/3e6bc84f87439f222f40b616e9a29a170f41fb564710ea510df19dc26907/soxr-1.1.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:34cc92208c3c412c046813e69da639c04a792c6a41fbfd7d909d359cd3e97a2d", size = 205699, upload-time = "2026-05-03T00:14:46.67Z" }, + { url = "https://files.pythonhosted.org/packages/2f/94/216f46096a85b07d1e6ba7fd44491402e912a3d688cd4f36f0a600ca155f/soxr-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bd30f7201eac896ebf5db7b09156e6f1a1b82601900d29d9c8449bdad8365b11", size = 167381, upload-time = "2026-05-03T00:14:48.012Z" }, + { url = "https://files.pythonhosted.org/packages/94/cb/06caa463b8181ec1981bd6376d4a873748b7008193188b8cfb60391eb131/soxr-1.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1577865e993f98ffb261257c3060fa76ec3db44ed3f181b16464268000424464", size = 210938, upload-time = "2026-05-03T00:14:49.768Z" }, + { url = "https://files.pythonhosted.org/packages/86/47/d5964551ca818b7f0c7ef7f3899056263b60ef098a801066350a9672ca8f/soxr-1.1.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3da87e3ffa3e41823d873b051c7ecb2acebd8d1b6b46b752f5facf10a0d84ab9", size = 245268, upload-time = "2026-05-03T00:14:51.422Z" }, + { url = "https://files.pythonhosted.org/packages/8f/29/371467eb86c7ba6810df0bfe9409bcd9c52ec5615b111190fafe23e4d2e1/soxr-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:ae30c48ac795378cf23ba3c7c640b8ff794af714ac388b9fd6b31a40b39e6e86", size = 176779, upload-time = "2026-05-03T00:14:53.09Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/f3da7973b5f1b05d2d7e94d5376b881dcbc05297900cae6c3d33d95b209b/soxr-1.1.0-cp312-abi3-macosx_10_14_x86_64.whl", hash = "sha256:e0e09fa633ce2e67df08b298afced4d184f6e753fc330f241022250f1d0d61da", size = 204124, upload-time = "2026-05-03T00:14:54.505Z" }, + { url = "https://files.pythonhosted.org/packages/03/dc/200013a74641f8774664bbcd2346c695c05c2e300ea792adcb40a293eed0/soxr-1.1.0-cp312-abi3-macosx_11_0_arm64.whl", hash = "sha256:d6a7ad82b8d5f3fcc04b1d2ca055562b96af571e1d4fa7c6c61d0fb509ac43b4", size = 165457, upload-time = "2026-05-03T00:14:56.007Z" }, + { url = "https://files.pythonhosted.org/packages/88/2b/2e5eba817a762a2ec589ff165b8bc5955b25a0ad140045f7cd8e45410543/soxr-1.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf98c0d7b7d5ef5bf072fee8d3020e8b664f2d195933ea7bc5089267c2e22a06", size = 206529, upload-time = "2026-05-03T00:14:57.646Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f1/0e55195893228609c9a08c3b13b7a83a46c3a992cd00d3304f0f320cfb07/soxr-1.1.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b033078e86f3c4a658e5697fac8995764fad9e799563616b630136b613167f1", size = 240413, upload-time = "2026-05-03T00:14:59.363Z" }, + { url = "https://files.pythonhosted.org/packages/b0/4d/621e4150e4815246ad552d215a8a294a90143fedd19ee442cf82d3b3abc8/soxr-1.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:6ae2a174bffea94e8ead857dad85999d3f49f091774dbad5b046c0417d7092f4", size = 174357, upload-time = "2026-05-03T00:15:00.724Z" }, + { url = "https://files.pythonhosted.org/packages/76/cd/77b74f1e95af0e11e52e9a034421aece7f7b45afd15a909afd41d5a5d102/soxr-1.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a941f5aaa0b8abced24318105c1ea22576afcc1138c19f625716ce4e2f76ad64", size = 207990, upload-time = "2026-05-03T00:15:02.1Z" }, + { url = "https://files.pythonhosted.org/packages/30/86/600cc31f982288167a59972746f117790162012546f995a32b5a55394b16/soxr-1.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:feebcba99ac99adb8009d46c8f4c1956b8c167576b0ae8a6fb47502e9a6f78e7", size = 169288, upload-time = "2026-05-03T00:15:03.75Z" }, + { url = "https://files.pythonhosted.org/packages/39/e4/80cd9aae0645513db1076d4384e8b2d895faf5009218b4a04348012c54fc/soxr-1.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:52c9ca84e3dc656d83acc424574770e20ea8e0704dc3842d4e27b0fe9d3ba449", size = 211405, upload-time = "2026-05-03T00:15:05.395Z" }, + { url = "https://files.pythonhosted.org/packages/a6/d6/cc3c80ac9b2289da4cf46c5d53b05e4327e6f5560a25868d06f9e2213af1/soxr-1.1.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4977323ef9c3aa3c2a26ff5fe0191c84b8fd759daf7afb1f25a91a55ad8b730", size = 244617, upload-time = "2026-05-03T00:15:07.134Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/f7af5fae841ffe32ed8440234ea2ad6adecca3bd92b6101076268c429000/soxr-1.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e17d4ef9b0185214b2c0935605ae63f827ea423bc74964be44763d68d2b6c21e", size = 187253, upload-time = "2026-05-03T00:15:08.813Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.41" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/66/45b165c595ec89aa7dcc2c1cd222ab269bc753f1fc7a1e68f8481bd957bf/sqlalchemy-2.0.41.tar.gz", hash = "sha256:edba70118c4be3c2b1f90754d308d0b79c6fe2c0fdc52d8ddf603916f83f4db9", size = 9689424, upload-time = "2025-05-14T17:10:32.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/4e/b00e3ffae32b74b5180e15d2ab4040531ee1bef4c19755fe7926622dc958/sqlalchemy-2.0.41-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6375cd674fe82d7aa9816d1cb96ec592bac1726c11e0cafbf40eeee9a4516b5f", size = 2121232, upload-time = "2025-05-14T17:48:20.444Z" }, + { url = "https://files.pythonhosted.org/packages/ef/30/6547ebb10875302074a37e1970a5dce7985240665778cfdee2323709f749/sqlalchemy-2.0.41-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9f8c9fdd15a55d9465e590a402f42082705d66b05afc3ffd2d2eb3c6ba919560", size = 2110897, upload-time = "2025-05-14T17:48:21.634Z" }, + { url = "https://files.pythonhosted.org/packages/9e/21/59df2b41b0f6c62da55cd64798232d7349a9378befa7f1bb18cf1dfd510a/sqlalchemy-2.0.41-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32f9dc8c44acdee06c8fc6440db9eae8b4af8b01e4b1aee7bdd7241c22edff4f", size = 3273313, upload-time = "2025-05-14T17:51:56.205Z" }, + { url = "https://files.pythonhosted.org/packages/62/e4/b9a7a0e5c6f79d49bcd6efb6e90d7536dc604dab64582a9dec220dab54b6/sqlalchemy-2.0.41-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c11ceb9a1f482c752a71f203a81858625d8df5746d787a4786bca4ffdf71c6", size = 3273807, upload-time = "2025-05-14T17:55:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/39/d8/79f2427251b44ddee18676c04eab038d043cff0e764d2d8bb08261d6135d/sqlalchemy-2.0.41-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:911cc493ebd60de5f285bcae0491a60b4f2a9f0f5c270edd1c4dbaef7a38fc04", size = 3209632, upload-time = "2025-05-14T17:51:59.384Z" }, + { url = "https://files.pythonhosted.org/packages/d4/16/730a82dda30765f63e0454918c982fb7193f6b398b31d63c7c3bd3652ae5/sqlalchemy-2.0.41-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03968a349db483936c249f4d9cd14ff2c296adfa1290b660ba6516f973139582", size = 3233642, upload-time = "2025-05-14T17:55:29.901Z" }, + { url = "https://files.pythonhosted.org/packages/04/61/c0d4607f7799efa8b8ea3c49b4621e861c8f5c41fd4b5b636c534fcb7d73/sqlalchemy-2.0.41-cp311-cp311-win32.whl", hash = "sha256:293cd444d82b18da48c9f71cd7005844dbbd06ca19be1ccf6779154439eec0b8", size = 2086475, upload-time = "2025-05-14T17:56:02.095Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8e/8344f8ae1cb6a479d0741c02cd4f666925b2bf02e2468ddaf5ce44111f30/sqlalchemy-2.0.41-cp311-cp311-win_amd64.whl", hash = "sha256:3d3549fc3e40667ec7199033a4e40a2f669898a00a7b18a931d3efb4c7900504", size = 2110903, upload-time = "2025-05-14T17:56:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/3e/2a/f1f4e068b371154740dd10fb81afb5240d5af4aa0087b88d8b308b5429c2/sqlalchemy-2.0.41-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:81f413674d85cfd0dfcd6512e10e0f33c19c21860342a4890c3a2b59479929f9", size = 2119645, upload-time = "2025-05-14T17:55:24.854Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e8/c664a7e73d36fbfc4730f8cf2bf930444ea87270f2825efbe17bf808b998/sqlalchemy-2.0.41-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:598d9ebc1e796431bbd068e41e4de4dc34312b7aa3292571bb3674a0cb415dd1", size = 2107399, upload-time = "2025-05-14T17:55:28.097Z" }, + { url = "https://files.pythonhosted.org/packages/5c/78/8a9cf6c5e7135540cb682128d091d6afa1b9e48bd049b0d691bf54114f70/sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a104c5694dfd2d864a6f91b0956eb5d5883234119cb40010115fd45a16da5e70", size = 3293269, upload-time = "2025-05-14T17:50:38.227Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/f74add3978c20de6323fb11cb5162702670cc7a9420033befb43d8d5b7a4/sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6145afea51ff0af7f2564a05fa95eb46f542919e6523729663a5d285ecb3cf5e", size = 3303364, upload-time = "2025-05-14T17:51:49.829Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d4/c990f37f52c3f7748ebe98883e2a0f7d038108c2c5a82468d1ff3eec50b7/sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b46fa6eae1cd1c20e6e6f44e19984d438b6b2d8616d21d783d150df714f44078", size = 3229072, upload-time = "2025-05-14T17:50:39.774Z" }, + { url = "https://files.pythonhosted.org/packages/15/69/cab11fecc7eb64bc561011be2bd03d065b762d87add52a4ca0aca2e12904/sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41836fe661cc98abfae476e14ba1906220f92c4e528771a8a3ae6a151242d2ae", size = 3268074, upload-time = "2025-05-14T17:51:51.736Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/0c19ec16858585d37767b167fc9602593f98998a68a798450558239fb04a/sqlalchemy-2.0.41-cp312-cp312-win32.whl", hash = "sha256:a8808d5cf866c781150d36a3c8eb3adccfa41a8105d031bf27e92c251e3969d6", size = 2084514, upload-time = "2025-05-14T17:55:49.915Z" }, + { url = "https://files.pythonhosted.org/packages/7f/23/4c2833d78ff3010a4e17f984c734f52b531a8c9060a50429c9d4b0211be6/sqlalchemy-2.0.41-cp312-cp312-win_amd64.whl", hash = "sha256:5b14e97886199c1f52c14629c11d90c11fbb09e9334fa7bb5f6d068d9ced0ce0", size = 2111557, upload-time = "2025-05-14T17:55:51.349Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ad/2e1c6d4f235a97eeef52d0200d8ddda16f6c4dd70ae5ad88c46963440480/sqlalchemy-2.0.41-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eeb195cdedaf17aab6b247894ff2734dcead6c08f748e617bfe05bd5a218443", size = 2115491, upload-time = "2025-05-14T17:55:31.177Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8d/be490e5db8400dacc89056f78a52d44b04fbf75e8439569d5b879623a53b/sqlalchemy-2.0.41-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d4ae769b9c1c7757e4ccce94b0641bc203bbdf43ba7a2413ab2523d8d047d8dc", size = 2102827, upload-time = "2025-05-14T17:55:34.921Z" }, + { url = "https://files.pythonhosted.org/packages/a0/72/c97ad430f0b0e78efaf2791342e13ffeafcbb3c06242f01a3bb8fe44f65d/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a62448526dd9ed3e3beedc93df9bb6b55a436ed1474db31a2af13b313a70a7e1", size = 3225224, upload-time = "2025-05-14T17:50:41.418Z" }, + { url = "https://files.pythonhosted.org/packages/5e/51/5ba9ea3246ea068630acf35a6ba0d181e99f1af1afd17e159eac7e8bc2b8/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc56c9788617b8964ad02e8fcfeed4001c1f8ba91a9e1f31483c0dffb207002a", size = 3230045, upload-time = "2025-05-14T17:51:54.722Z" }, + { url = "https://files.pythonhosted.org/packages/78/2f/8c14443b2acea700c62f9b4a8bad9e49fc1b65cfb260edead71fd38e9f19/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c153265408d18de4cc5ded1941dcd8315894572cddd3c58df5d5b5705b3fa28d", size = 3159357, upload-time = "2025-05-14T17:50:43.483Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b2/43eacbf6ccc5276d76cea18cb7c3d73e294d6fb21f9ff8b4eef9b42bbfd5/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f67766965996e63bb46cfbf2ce5355fc32d9dd3b8ad7e536a920ff9ee422e23", size = 3197511, upload-time = "2025-05-14T17:51:57.308Z" }, + { url = "https://files.pythonhosted.org/packages/fa/2e/677c17c5d6a004c3c45334ab1dbe7b7deb834430b282b8a0f75ae220c8eb/sqlalchemy-2.0.41-cp313-cp313-win32.whl", hash = "sha256:bfc9064f6658a3d1cadeaa0ba07570b83ce6801a1314985bf98ec9b95d74e15f", size = 2082420, upload-time = "2025-05-14T17:55:52.69Z" }, + { url = "https://files.pythonhosted.org/packages/e9/61/e8c1b9b6307c57157d328dd8b8348ddc4c47ffdf1279365a13b2b98b8049/sqlalchemy-2.0.41-cp313-cp313-win_amd64.whl", hash = "sha256:82ca366a844eb551daff9d2e6e7a9e5e76d2612c8564f58db6c19a726869c1df", size = 2108329, upload-time = "2025-05-14T17:55:54.495Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fc/9ba22f01b5cdacc8f5ed0d22304718d2c758fce3fd49a5372b886a86f37c/sqlalchemy-2.0.41-py3-none-any.whl", hash = "sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576", size = 1911224, upload-time = "2025-05-14T17:39:42.154Z" }, +] + +[[package]] +name = "standard-aifc" +version = "3.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { 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 = [ + { url = "https://files.pythonhosted.org/packages/c3/52/5fbb203394cc852334d1575cc020f6bcec768d2265355984dfd361968f36/standard_aifc-3.13.0-py3-none-any.whl", hash = "sha256:f7ae09cc57de1224a0dd8e3eb8f73830be7c3d0bc485de4c1f82b4a7f645ac66", size = 10492, upload-time = "2024-10-30T16:01:07.071Z" }, +] + +[[package]] +name = "standard-chunk" +version = "3.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/06/ce1bb165c1f111c7d23a1ad17204d67224baa69725bb6857a264db61beaf/standard_chunk-3.13.0.tar.gz", hash = "sha256:4ac345d37d7e686d2755e01836b8d98eda0d1a3ee90375e597ae43aaf064d654", size = 4672, upload-time = "2024-10-30T16:18:28.326Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/90/a5c1084d87767d787a6caba615aa50dc587229646308d9420c960cb5e4c0/standard_chunk-3.13.0-py3-none-any.whl", hash = "sha256:17880a26c285189c644bd5bd8f8ed2bdb795d216e3293e6dbe55bbd848e2982c", size = 4944, upload-time = "2024-10-30T16:18:26.694Z" }, +] + +[[package]] +name = "standard-sunau" +version = "3.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { 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 = [ + { url = "https://files.pythonhosted.org/packages/34/ae/e3707f6c1bc6f7aa0df600ba8075bfb8a19252140cd595335be60e25f9ee/standard_sunau-3.13.0-py3-none-any.whl", hash = "sha256:53af624a9529c41062f4c2fd33837f297f3baa196b0cfceffea6555654602622", size = 7364, upload-time = "2024-10-30T16:01:28.003Z" }, +] + +[[package]] +name = "starlette" +version = "0.46.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846, upload-time = "2025-04-13T13:56:17.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload-time = "2025-04-13T13:56:16.21Z" }, +] + +[[package]] +name = "submitit" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cloudpickle" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/86/497018fb3b74e71bef45df82762b176e6b3d159f29941c20d2f141ec4096/submitit-1.5.4.tar.gz", hash = "sha256:7100848bd1cdda79c7196e54ee830793ae75fd7adde0c5bef738d72360a07508", size = 81538, upload-time = "2025-12-17T19:20:03.396Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/bb/711e1c2ebd18a21202c972dd5d5c8e09a921f2d3560e3a53d6350c808ab7/submitit-1.5.4-py3-none-any.whl", hash = "sha256:c26f3a7c8d4150eaf70b1da71e2023e9e9936c93e8342ed7db910f29158561c5", size = 76043, upload-time = "2025-12-17T19:20:01.941Z" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + +[[package]] +name = "torch" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-bindings", marker = "python_full_version < '3.15' and sys_platform == 'linux'" }, + { name = "cuda-toolkit", extra = ["cublas", "cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "sys_platform == 'linux'" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx" }, + { name = "nvidia-cudnn-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu13", marker = "sys_platform == 'linux'" }, + { name = "setuptools" }, + { name = "sympy" }, + { name = "triton", marker = "python_full_version < '3.15' and sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/fe/cba54dc58523434919b66f13a667e36e436deddd77ca519e96553617d4ec/torch-2.13.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:e76f9bcecc52b8ff711239a2f7547d5353df95878ab232f0773c1d95928b92f8", size = 111187938, upload-time = "2026-07-08T16:05:17.065Z" }, + { url = "https://files.pythonhosted.org/packages/c2/59/1e3160e18e12aa3038390efab3ce02b36a9d4d6a527ecdd8520dca2e68d8/torch-2.13.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:092790c696a760c729fd5722835f50b9d81fd7c8f141571f3f3cf4081a8f664c", size = 427199369, upload-time = "2026-07-08T16:04:51.054Z" }, + { url = "https://files.pythonhosted.org/packages/01/79/1f2d34ad7034ee1c7ffc1cf8bf0f8213af2a81df6ecdb3997ecec107c09d/torch-2.13.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:60fcdcb2f3876e21146cb4524ef06397d727ca9ad5f020818547e25075fe3cb7", size = 526574961, upload-time = "2026-07-08T16:04:07.075Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fd/0f2ce40f58aefbdb3392f9acce3c8171940943ae2d661f70558bfa73befb/torch-2.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:a0d8b11f16a48d60e2015d8213aa0390744cbebb98e58b62b3514dddc656e330", size = 122015870, upload-time = "2026-07-08T16:05:27.59Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3a/ed0f4d4d1dcde03bced7aac9a28e800abcdc0cbd06b6775044c9fbd877b7/torch-2.13.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2fe228aba290d14b9f31b049be550dbd469c3fd3013d7a19705b30454da97027", size = 111213045, upload-time = "2026-07-08T16:05:22.997Z" }, + { url = "https://files.pythonhosted.org/packages/df/a9/f6a2a4d763ff1df02e9a64c477029db614295bc9367f4131223791ccc243/torch-2.13.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:572df8be8ffb4599c88cbd6a0726f1f854f4da65d2e3c09f0e2c2283333cd6d4", size = 427210998, upload-time = "2026-07-08T16:04:37.708Z" }, + { url = "https://files.pythonhosted.org/packages/f3/82/fea946351658e6534db52d2cc12bc53087cbf87f9440c5f180f367c1950b/torch-2.13.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:796633c4cdf0fe2cdced72d8f88f22e73dbcfce83132763162f6d4bff13b820b", size = 526605292, upload-time = "2026-07-08T16:04:22.81Z" }, + { url = "https://files.pythonhosted.org/packages/21/d6/e8f3c6f7e01f626f77259de9860d2a78bc84c40539e28e79b7e98b0bb659/torch-2.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:024c6cc0c1b085f2f91f20a3dc27b0471d021c31ce84b81be3afdc39f791fd9d", size = 122057313, upload-time = "2026-07-08T16:03:53.43Z" }, + { url = "https://files.pythonhosted.org/packages/0d/fa/c1c10b7aff4a9a3e8956d4f0a5f468fa6db7abc3208805719076772b4833/torch-2.13.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:33449899ce5496c1b84b4853179d94fd102028ae1407314d9fb956bb79e70d09", size = 111213743, upload-time = "2026-07-08T16:03:28.579Z" }, + { url = "https://files.pythonhosted.org/packages/11/18/9ecb37b56293a0be8d80f810bf672a72fe7e02f8b475d5ef1b9bf8a0d748/torch-2.13.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:1e09d6a722504957c694faceca843acde562786df1144ebcc5a74075ec7f6005", size = 427213008, upload-time = "2026-07-08T16:03:44.106Z" }, + { url = "https://files.pythonhosted.org/packages/d4/5a/7c50ba1b7b713d71d34669c6d13dab0a11531a3eceb0307a5162dbfec0f7/torch-2.13.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:a3a9a21312872af8a26950b2c15680335a386a1f56ed03e780653d78b9607e9e", size = 526602329, upload-time = "2026-07-08T16:03:12.649Z" }, + { url = "https://files.pythonhosted.org/packages/91/3d/e7adcc6aaf36961cd18f56cf8ad0f3058c3a5c84ccf391762176c94581b8/torch-2.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:49b58f1e2c52440abb6f17c28f0335fe6c6d01ad1a7f55b0183b81e4b34d64e6", size = 122057920, upload-time = "2026-07-08T16:03:01.808Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/6dcc7f0c07052102dd36f83cbc5800842a909c8c3fbf1a7f8a5844954de9/torch-2.13.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d849b390e07d8d333ce8ecaf91b273c656c598379a19c9acf1318a883f6b391c", size = 111227066, upload-time = "2026-07-08T16:03:33.6Z" }, + { url = "https://files.pythonhosted.org/packages/e9/09/2c10e8cd0e00fa5d23c052df6ce467eaa7182399f5e0f824f1e4ff42ccae/torch-2.13.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:a3893dc2da0a972a8ca5d698c85a9f967559ac5f8ee1797b77408aa8734d073c", size = 427226309, upload-time = "2026-07-08T16:02:53.127Z" }, + { url = "https://files.pythonhosted.org/packages/76/c6/22c2102bbef14ca6a6cb4c20e42f088e49c5f812be4e160ae57502e325f9/torch-2.13.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:49f1ea385c754e54919408a9bb3b5a72b0b755bbe2c916c1d6f70afbec4908a2", size = 526614507, upload-time = "2026-07-08T16:02:16.441Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0c/7d1deb6bce5bc3e6042caf39100ac768eba3b9a098e1dddd16f75bd6489b/torch-2.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f8573e3ce9ebcd53fe922f01077a6085ccdfbe5f12fd215883a9d87d7a744fd", size = 122051871, upload-time = "2026-07-08T16:03:23.521Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ce/aa8b7f9949d32e0f2f624f342bc3b48112c1b8a130288465938bc83bcbf9/torch-2.13.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c28def70706c2f9ecc752574766e8ae4da9b810ab6676b611166761a78a9f1e1", size = 111537025, upload-time = "2026-07-08T16:02:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/69/d1/491e3a0389430946145888b0203f2b6a759ce2a61481b96a85c2da4f2ced/torch-2.13.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:31061ff56ed8fbf26c749806905aeb749ebeb819810fd5d52508aa5afd90dddc", size = 427219769, upload-time = "2026-07-08T16:02:31.18Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1d/38006e045bf0a1fc28ef01e757c554e59e59a8770c284bc4f47b14e60441/torch-2.13.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:cc26eead4cf51d0b544e31e364dcf000846549c273bd148936fe9d24d29acb92", size = 526571320, upload-time = "2026-07-08T16:01:59.348Z" }, + { url = "https://files.pythonhosted.org/packages/56/94/655c91992a882bd5071aa0b5d22a07dbb130d801e872be97c0b627a7c693/torch-2.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a7de8a313090dc5c7d7ba4bfe5c3be222528f9a4dba1acc83bddb1157360c4b8", size = 122306773, upload-time = "2026-07-08T16:02:39.832Z" }, +] + +[[package]] +name = "torchaudio" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/77/0eec7f175d88f312296bd5b11c23bd58da37c1021f53da3db4df449ce3ee/torchaudio-2.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:492dd64645e9d0bb843e94f1d9a4d1e31426262ffc594fafecc1697df9df5eb9", size = 684142, upload-time = "2026-03-23T18:13:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/b3/f9/6f7ebe071b44592c85269762b55b63ab0a091b5f479f73544738f7564a1e/torchaudio-2.11.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:73dab4841f94d888bc7c2aed7b5547c643edc974306919fe1adfb65d57cccf4b", size = 1626527, upload-time = "2026-03-23T18:13:39.011Z" }, + { url = "https://files.pythonhosted.org/packages/ac/70/17408e0d154d0c894537a88dcbadc48e8ad3b6e1ef4a1dabda5d40245ee0/torchaudio-2.11.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1a07ec72fd6f26a588c39b5f029e0130d16bb40bc4221635580bf8fb18fcbc80", size = 1771930, upload-time = "2026-03-23T18:13:37.963Z" }, + { url = "https://files.pythonhosted.org/packages/c9/75/b6d03fc75b409bdaec597274d1bdd4213db716ed16f6801386b31d59c551/torchaudio-2.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:bb59ba4452bbbe95d75ad3ef18df9824955625f36698ce9a5998a4a9f3c1ba1d", size = 328658, upload-time = "2026-03-23T18:13:44.545Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b1/77658817acacd01a72b714440c62f419efc4d90170e704e8e7a2c0918988/torchaudio-2.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a1cf1acc883bee9cb906a933572fed6a8a933f86ef34e9ea7d803f72317e8c1b", size = 684226, upload-time = "2026-03-23T18:13:40.023Z" }, + { url = "https://files.pythonhosted.org/packages/78/28/c7adc053039f286c2aca0038b766cbe3294e66fec6b29a820e95128f9ede/torchaudio-2.11.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:bc653defca1c16154398517a1adc98d0fb7f1dd08e58ced217558d213c2c6e29", size = 1626670, upload-time = "2026-03-23T18:13:42.162Z" }, + { url = "https://files.pythonhosted.org/packages/88/d8/d6d0f896e064aa67377484efef4911cdcc07bce2929474e1417cc0af18c2/torchaudio-2.11.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6503c0bdb29daf2e6281bb70ea2dfe2c3553b782b619eb5d73bdadd8a3f7cecf", size = 1771992, upload-time = "2026-03-23T18:13:33.188Z" }, + { url = "https://files.pythonhosted.org/packages/23/a8/941277ecc39f7a0a169d554302a1f1afd87c1d94a8aec828891916cea59a/torchaudio-2.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:478110f981e5d40a8d82221732c57a56c85a1d5895fb8fe646e86ee15eded3bd", size = 328663, upload-time = "2026-03-23T18:13:19.218Z" }, + { url = "https://files.pythonhosted.org/packages/fb/9e/f76fcd9877c8c78f258ee34e0fb8291fdb91e6218d582d9ca66b1e4bd4ae/torchaudio-2.11.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e3f9696a9ef1d49acc452159b052370c636406d072e9d8f10895fda87b591ea9", size = 679904, upload-time = "2026-03-23T18:13:28.329Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/249c1498ebdad3e7752866635ec0855fc0dcf898beccda5a9d2b9df8e4d0/torchaudio-2.11.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:b034d7672f1c415434f48ef17807f2cce47f29e8795338c751d4e596c9fbe8b5", size = 1618523, upload-time = "2026-03-23T18:13:15.703Z" }, + { url = "https://files.pythonhosted.org/packages/4f/98/be13fe35d9aa5c26381c0e453c828a789d15c007f8f7d08c95341d19974d/torchaudio-2.11.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1c1101c1243ef0e4063ec63298977e2d3655c15cf88d9eb0a1bd4fe2db9f47ea", size = 1771992, upload-time = "2026-03-23T18:13:35.343Z" }, + { url = "https://files.pythonhosted.org/packages/e2/8b/2bbb3dca6ff28cba0de250874d5ef4fc2822c47a934b59b3974cff3219ef/torchaudio-2.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:986f4df5ed17b003dc52489468601720090e65f964f8bebccf90eb45bba75744", size = 328662, upload-time = "2026-03-23T18:13:18.308Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ce/52c652d30af7d6e96c8f1735d26131e94708e3f38d852b8fa97958804dd8/torchaudio-2.11.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:bda09ea630ae7207384fb0f28c35e4f8c0d82dd6eba020b6b335ad0caa9fed49", size = 680814, upload-time = "2026-03-23T18:13:17.08Z" }, + { url = "https://files.pythonhosted.org/packages/06/95/1ad1507482e7263e556709a3f5f87fecd375a0742cdaf238806c8e72eaad/torchaudio-2.11.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:9fe3083c62e035646483a14e180d33561bdc2eed436c9ab1259c137fb7120b4a", size = 1618546, upload-time = "2026-03-23T18:13:29.686Z" }, + { url = "https://files.pythonhosted.org/packages/98/4c/480328ba07487eb9890406720304d0d460dd7a6a64098614f5aa53b662ca/torchaudio-2.11.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:13cff988697ccbad539987599f9dc672f40c417bed67570b365e4e5002bbd096", size = 1771991, upload-time = "2026-03-23T18:13:30.843Z" }, + { url = "https://files.pythonhosted.org/packages/3e/98/5d4790e2d6548768999acd34999d5aeefce8bcc23a07afaa5f03e723f557/torchaudio-2.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ed404c4399ad7f172c86a47c1b25293d322d1d58e26b10b0456a86cf67d37d84", size = 328661, upload-time = "2026-03-23T18:13:34.359Z" }, + { url = "https://files.pythonhosted.org/packages/39/fe/ffa618b4f0d9732d7df7a2fa2bd48657d896599bc224e5af3c70d46c546b/torchaudio-2.11.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:cc09cd1f6015b8549e7fe255fb1be5346b57e7fee06541d3f3dbb012d8c4715f", size = 679901, upload-time = "2026-03-23T18:13:25.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/54/f414d7b92dd0b3094a2409c95a97bd6c49aa0620da722a0e55462f9bd9cb/torchaudio-2.11.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:79fb3cb99169fd41bd9719647261402a164da0d105a4d81f42a3260844ec5e79", size = 1618527, upload-time = "2026-03-23T18:13:26.68Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a8/bf2e1f6ce24c990192400ae49b4acc1a0d0295b6c6a06bceecdc46ce08de/torchaudio-2.11.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:00e9f71ab9c656f0abdb40c515bd65d4658ab0ad380dee27a2efd7d51dabd3d6", size = 1771995, upload-time = "2026-03-23T18:13:23.373Z" }, + { url = "https://files.pythonhosted.org/packages/83/6f/b0efb44e0bfe8dd4d78d76ae3be280354e1fb5c8631c782785d74cd8a7b1/torchaudio-2.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:1424638adb8bb40087bc7b6eb103e8e4fe398210f09076f33b7b5e61501b5d66", size = 328662, upload-time = "2026-03-23T18:13:32.243Z" }, + { url = "https://files.pythonhosted.org/packages/60/84/1c792b0b700eac9a96772cfd9f96c097b17bca3234a2fde3c64b8063660d/torchaudio-2.11.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:da2725e250866da42a12934c9a6552f65a18b7187fd7a6221387f0e605fb3b96", size = 679926, upload-time = "2026-03-23T18:13:24.452Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a0/62a5842062f739239691f2e57523e0570dd06704ad987755f7644a3afa23/torchaudio-2.11.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:1be3767064364ae82705bdf2b15c1e8b41fea82c4cd04d47428a8684b634b6ed", size = 1618552, upload-time = "2026-03-23T18:13:21.09Z" }, + { url = "https://files.pythonhosted.org/packages/6d/89/c293d818f9f899db93bf291b42401c05ae29acfb2e53d5341c30ea703e62/torchaudio-2.11.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:67f6edac29ed004652c11db5c19d9debb5d835695930574f564efc8bdd061bba", size = 1771986, upload-time = "2026-03-23T18:13:22.153Z" }, + { url = "https://files.pythonhosted.org/packages/93/f7/ee5da8c03f1a3c7662c6c6a119f24a4b3e646da94be56dce3201e3a6ee9b/torchaudio-2.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:88fb5e29f670a33d9bac6aabb1d2734460cf6e461bde5cdc352826035851b16d", size = 328661, upload-time = "2026-03-23T18:13:20.1Z" }, +] + +[[package]] +name = "tqdm" +version = "4.69.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/69/40407dfc835517f058b603dbf37a6df094d8582b015a51eddc988febbcb7/tqdm-4.69.0.tar.gz", hash = "sha256:700c5e85dcd5f009dd6222588a29180a193a748247a5d855b4d67db93d79a53b", size = 792569, upload-time = "2026-07-17T18:09:06.2Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/21/99a0cdaf54eb35e77623c41b5a2c9472ee4404bba687052791fe2aba6773/tqdm-4.69.0-py3-none-any.whl", hash = "sha256:9979978912be667a6ef21fd5d8abf54e324e63d82f7f43c360792ebc2bc4e622", size = 676680, upload-time = "2026-07-17T18:09:04.172Z" }, +] + +[[package]] +name = "treetable" +version = "0.2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/f1/e5f28b2485d8d3169ff0167e3e560fa912a96e71916bf11365c9e40f11f5/treetable-0.2.6.tar.gz", hash = "sha256:7e1d62dbce503fbf24561aee1461b8fbcc2c232ff45661c3b9d0c2081c795bdf", size = 9577, upload-time = "2025-09-02T20:40:06.557Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/16/dd00f9fc5b84cb3fb396d62245e11761accfd9d27fd56ce0024bdd38a0ae/treetable-0.2.6-py3-none-any.whl", hash = "sha256:fa7dfa0297d2bbc5882191edd2e15f79a5e883e352f489e2acadb221db565adf", size = 7379, upload-time = "2025-09-03T18:57:17.784Z" }, +] + +[[package]] +name = "triton" +version = "3.7.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/f9/19d842d06a08559534fa1eaab6ca551b1bcf40f06620bddec1babaa2772d/triton-3.7.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4a0e1cd4c4a76370ed74a8432a53cea28716827d19e40ffc732233e35ceb3f6", size = 184664887, upload-time = "2026-06-17T20:03:42.913Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5e/fce69606f7f240297f163e25539906732b199530d486ce67ae319877e821/triton-3.7.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6744957e9fd610a29680ec2346057d0c86948ed3812468670719f391e94b44a5", size = 197701306, upload-time = "2026-06-17T19:53:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/94/fa/f856e24deb462d5f18bd4b5a746957862ab9b6ee5834bda60605ec348366/triton-3.7.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9497f2e696ee368862a181a90b2dcc03ca978cc4f602abd67c7d81022a6988e1", size = 184692359, upload-time = "2026-06-17T20:03:48.288Z" }, + { url = "https://files.pythonhosted.org/packages/c4/6f/fb96d15db6f36d6eae4cafb998c2e0353bf59d7c4ea1662d7497f269134a/triton-3.7.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e40869937a68206ec70d7f25bb7ec6433cb083f9135e1f36dbd318dc449a728", size = 197719725, upload-time = "2026-06-17T19:53:20.419Z" }, + { url = "https://files.pythonhosted.org/packages/00/42/c5089d4d9327fcd1e862c599cc2927f39418f84dd11a84cb2ccff9d4787a/triton-3.7.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cdbfc09d9ec58bc5e68321525653220de7515c199e7a8097a97c85e62b52cd0a", size = 184694629, upload-time = "2026-06-17T20:03:53.444Z" }, + { url = "https://files.pythonhosted.org/packages/07/42/2c3ac59253ae8892b6f307875263dd23dc875cdf732d3aea40d6d41fb7cb/triton-3.7.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58c0e131da05134a2a4788ccbcc0c1105cf0f54c8e98f19e34cd465396dc15eb", size = 197729241, upload-time = "2026-06-17T19:53:27.801Z" }, + { url = "https://files.pythonhosted.org/packages/40/71/e01aa7ad573883ed9456f130226babdec70b005e098c4d6226a6238e761b/triton-3.7.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe4ea396a06171f1f1f58cbd39c70b09294398f7dd7c620939bab54ad6f934fa", size = 184705764, upload-time = "2026-06-17T20:03:59.064Z" }, + { url = "https://files.pythonhosted.org/packages/a4/09/5683146fda6a2b569deb78ccfd8fbfea8bfe55f726b081c0a6bb18dd6f28/triton-3.7.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2020153b08280415ec0da6607834e79166442147e78e144df06b508c75b186d2", size = 197729537, upload-time = "2026-06-17T19:53:35.516Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f8/448220c3092019f9fdfab39ec47985968181d67da34b44f6a7f6280a5cbb/triton-3.7.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c58e4c61f0c73b5dba3b5d19b4a7093c32f90dc18b2a7f121a7c16ccd31107b7", size = 184814760, upload-time = "2026-06-17T20:04:04.984Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ac/229b7d4589d2e5937310e72c6d46e89599d16a4a12b479ffa1499fee8eb8/triton-3.7.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10ba85fa2cca4a2fbdeb36bf1cb082f2c252bda55bf9fccd74f65ec5bc647e68", size = 197824404, upload-time = "2026-06-17T19:53:42.772Z" }, +] + +[[package]] +name = "trove-classifiers" +version = "2026.6.1.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/e3/7ca82ee24c82d344584abd5b8637b3bd056f2900226e8d82fc22f1184b92/trove_classifiers-2026.6.1.19.tar.gz", hash = "sha256:c5132b4b61a829d11cfbd2d72e97f20a45ed6edb95e45c5efdeb5e00836b2745", size = 17059, upload-time = "2026-06-01T19:41:34.649Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/a4/81502f486f01db95bc8320646a8a12511f5e556cb63d5e224d91816605c4/trove_classifiers-2026.6.1.19-py3-none-any.whl", hash = "sha256:ab4c4ec93cc4a4e7815fa759906e05e6bb3f2fbd92ea0f897288c6a43efd15b3", size = 14211, upload-time = "2026-06-01T19:41:33.434Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/ae/9bbb19b9e1c450cf9ecaef06463e40234d98d95bf572fab11b4f19ae5ded/uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328", size = 76815, upload-time = "2025-04-19T06:02:50.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/4b/4cef6ce21a2aaca9d852a6e84ef4f135d99fcd74fa75105e2fc0c8308acd/uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403", size = 62483, upload-time = "2025-04-19T06:02:48.42Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, + { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" }, + { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" }, + { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" }, + { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838", size = 108252, upload-time = "2026-05-18T04:32:04.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/3d/8024c801df84d1587740d0359e7fdd80afeae3d159011f3d5376dd82f18e/watchfiles-1.2.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:704fd259e332e01f9b9c178f4bce9e49027e5587cc2600eeeaf8e76e1c846201", size = 400242, upload-time = "2026-05-18T04:31:19.014Z" }, + { url = "https://files.pythonhosted.org/packages/87/5b/f4dfd45323e949984a3a7f9dc31d1cbb049921e7d98253488dda72ccdaa9/watchfiles-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6543cf55d170003296d185c0af981f3e1311564907e1f4e08671fc7693a890a5", size = 394562, upload-time = "2026-05-18T04:30:08.46Z" }, + { url = "https://files.pythonhosted.org/packages/98/d8/19483ef075d601c409bce8bcbb5c0f81a10876fff870400568f08ce484a1/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89d8c2394a065ca86f5d2910ff263ae67c127e1376ccc4f9fc35c71db879f80a", size = 456611, upload-time = "2026-05-18T04:30:45.723Z" }, + { url = "https://files.pythonhosted.org/packages/b1/6a/cc81fbe7ee42f2f22e661a6e12def7807e01b14b2f39e0ff83fd373fd307/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:772b80df316480d894a0e3165fdd19cf77f5d17f9a787f94029465ad0e3529d1", size = 461379, upload-time = "2026-05-18T04:31:29.292Z" }, + { url = "https://files.pythonhosted.org/packages/b1/57/7e669002082c0a0f4fb5113bb70125f7110124b846b0a11bc5ae8e90eac1/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d158cd89df6053823533e06fb1d73c549133bff5f0396170c0e53d9559340717", size = 493556, upload-time = "2026-05-18T04:30:05.44Z" }, + { url = "https://files.pythonhosted.org/packages/45/7d/f60a2b19807b21fe8281f3a8da4f59eef0d5f96825ac4680ba2d4f2ebf91/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d516b3283a758e087841aedb8031549fb41ced08f3db10aa6d2bf32dc042525b", size = 575255, upload-time = "2026-05-18T04:30:40.568Z" }, + { url = "https://files.pythonhosted.org/packages/bd/49/77f5b5e6efbcd57482f74948ebb1b97e5c0046d6b61475042d830c84b3ff/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53b2290c92e0506d102cd448fbc610d87079553f86caa39d67440856a8b8bba5", size = 467052, upload-time = "2026-05-18T04:31:17.942Z" }, + { url = "https://files.pythonhosted.org/packages/ee/5a/73e2959af1b97fd5d556f9a8bdba017be23ceeef731869d5eaa0a753d5a3/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a711b51aec4370d0dcda5b6c09463206f133a5759341d7744b953a7b62e1100e", size = 456858, upload-time = "2026-05-18T04:30:30.182Z" }, + { url = "https://files.pythonhosted.org/packages/50/57/1bc8c27fad7e6c19bddee15d276dbb6ab72480ec01c127afff1673aee417/watchfiles-1.2.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:e2ca07fa7d89195ec0865d3d285666286740bfa83d83e5cee204043a31ecc165", size = 467579, upload-time = "2026-05-18T04:32:15.897Z" }, + { url = "https://files.pythonhosted.org/packages/09/6c/3c2e44edba3553c5e3c3b8c8a2a6dee6b9e12ae2cf4bd2378bebf9dc3038/watchfiles-1.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e0618518f282c4ebff60f5e5b1247b6d91bb8b9f4476947563a1e74acc66f3c6", size = 633253, upload-time = "2026-05-18T04:31:37.123Z" }, + { url = "https://files.pythonhosted.org/packages/30/c2/d8c84a882ab39bbefcc4915ab3e91830b7a7e990c5570b0b69075aba3faf/watchfiles-1.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0d191c054d0715c3c95c99df9b8dbf6fd096d8c1e021e8f212e1bd8bc444ccb5", size = 660713, upload-time = "2026-05-18T04:31:24.62Z" }, + { url = "https://files.pythonhosted.org/packages/a9/07/f97736a5fc605364fe67b25e9fa4a6965dfd4840d50c406ada507e9d735f/watchfiles-1.2.0-cp311-cp311-win32.whl", hash = "sha256:9342472aff9b093c5acd4f6d8f70ae0937964ab56542502bcf5579782da69ae8", size = 277222, upload-time = "2026-05-18T04:31:21.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/99/2b04981977fc2608afd60360d928c6aecf6b950292ca221d98f4005f6694/watchfiles-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:dbd6c97045dad81227c8d040173da044c1de08de64a5ea8b555da4aee1d5fa22", size = 290274, upload-time = "2026-05-18T04:31:45.966Z" }, + { url = "https://files.pythonhosted.org/packages/3c/74/f7f58a7075ee9cf612b0cfcddb78b8cd8234f0742d6f0075cf0da2dde1c6/watchfiles-1.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:57a2d9fa4fb4c2ecae57b13dfff2c7ab53e21a2ba674fe9f05506680fcdcc0d7", size = 283460, upload-time = "2026-05-18T04:31:39.126Z" }, + { url = "https://files.pythonhosted.org/packages/b8/2f/e42c992d2afda3108ea1c02acecc991b9f31d05c14adc2a7cee9ee211fc4/watchfiles-1.2.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bc13eb17538be00c874699dc0abe4ee2bc8d50bb1166a6b9e175ef3fd7eb8f26", size = 400115, upload-time = "2026-05-18T04:32:02.06Z" }, + { url = "https://files.pythonhosted.org/packages/5f/8f/6af2ea19065c91d8b0ea3516fdfc8c0d349f407e8e9fbf4e5a17360de8ad/watchfiles-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c", size = 393659, upload-time = "2026-05-18T04:30:50.951Z" }, + { url = "https://files.pythonhosted.org/packages/13/01/b32a967c56fb3e3e5be3db52c3d3b87fa4513aa367d8ed1ad96d42952e5f/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc", size = 453207, upload-time = "2026-05-18T04:31:04.231Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/97557a812180338cb1abd32e1cffcc4588f59b5f23e0cb006b2ba95ba64a/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0", size = 459273, upload-time = "2026-05-18T04:31:50.377Z" }, + { url = "https://files.pythonhosted.org/packages/e8/a8/b4b08dcb7653b8087c6586f7ce649505900e866bbcfe40dc9587af02e686/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2581a94056e55d7d0a31a823ea92bf73749c489ca2285bfdc0fbe6b2bb49d50c", size = 489927, upload-time = "2026-05-18T04:31:42.485Z" }, + { url = "https://files.pythonhosted.org/packages/50/94/3dceea03545d2e5ddfd839f0ddd5e1cecbf1697b5a428d5ba11cef6af95d/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01", size = 570476, upload-time = "2026-05-18T04:31:03.071Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f2/d39a5450c3532092b91f81d274360e613c2371bc874a89c7a1a3c5e8d138/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8", size = 465650, upload-time = "2026-05-18T04:30:12.701Z" }, + { url = "https://files.pythonhosted.org/packages/22/24/ed72f68cbc1333ca9b9f2200aa048bb6658ae41709bc1caad4310f4bdffd/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5", size = 456398, upload-time = "2026-05-18T04:30:13.784Z" }, + { url = "https://files.pythonhosted.org/packages/0d/64/982ef4a4e5bab5b6e5b6becc8cd5e732f6130a78b855f0abec6439a9a135/watchfiles-1.2.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d", size = 465140, upload-time = "2026-05-18T04:31:52.111Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0c/95282abf4ed680b6096010bcfc30c5fa7a041fc5aa5a2ad17a2cc6c75bba/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c", size = 630259, upload-time = "2026-05-18T04:31:25.676Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/607c1de1530c4bdcf2cf1d1ecc2505ddba5d96bd43ba9f2b0e79876f850f/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906", size = 659859, upload-time = "2026-05-18T04:30:24.333Z" }, + { url = "https://files.pythonhosted.org/packages/fa/08/d9e2e0f9e8e6791d33aefc694ad7eefa7f901f63caff84a81ded38692f9c/watchfiles-1.2.0-cp312-cp312-win32.whl", hash = "sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898", size = 275480, upload-time = "2026-05-18T04:30:31.307Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e6/9d42569c0102645cc8cea5d8c7d8a1e9d4ada2cb7f05f75e554b8aa2202a/watchfiles-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379", size = 288718, upload-time = "2026-05-18T04:32:10.745Z" }, + { url = "https://files.pythonhosted.org/packages/0a/26/88e0dc6ee3898169d7fa22bb6a69cabf2502d2ee25cb8c876d1262d204f8/watchfiles-1.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5", size = 281026, upload-time = "2026-05-18T04:30:22.23Z" }, + { url = "https://files.pythonhosted.org/packages/d1/4d/70a7feced9f87e2ff26dba42667290f41694fc64646c67261fbb8cab5d5c/watchfiles-1.2.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98", size = 399730, upload-time = "2026-05-18T04:31:38.162Z" }, + { url = "https://files.pythonhosted.org/packages/31/3a/0da302f2307aee316922806ebd5726c542cbd787c938271cf14a074c7daf/watchfiles-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44", size = 392842, upload-time = "2026-05-18T04:30:27.051Z" }, + { url = "https://files.pythonhosted.org/packages/db/ef/d5bdb705c224dbc256aa0c1ec47bf4e61ec52558f2afb44a71a1fe4d7015/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658", size = 452989, upload-time = "2026-05-18T04:31:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/5495f2c1661949ef7a35e4d71111d129cfe7606414a26887a919d0a55406/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb", size = 458978, upload-time = "2026-05-18T04:30:52.606Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/7f9c07c433811c2fffd93e13fdfb7135de9aab5f2ae41be08960fa0047dc/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f", size = 490248, upload-time = "2026-05-18T04:31:36.003Z" }, + { url = "https://files.pythonhosted.org/packages/3c/11/d93632febc52fbc21be90231bb7c17fd5387f46c9076fd40a5f9c2ae6910/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0", size = 571847, upload-time = "2026-05-18T04:31:10.862Z" }, + { url = "https://files.pythonhosted.org/packages/55/b4/383173e73aabb07ad1d9c7aa859d95437ac46a6d6a1e11005facda0c9d19/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5", size = 465974, upload-time = "2026-05-18T04:30:17.006Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6c/89b1a230a78f57c52dd8893adb1f92f94411721b6ec12596c56d98c74356/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71", size = 454782, upload-time = "2026-05-18T04:30:35.656Z" }, + { url = "https://files.pythonhosted.org/packages/24/62/1732118367cfff0a9fce3bf62ff4bfded09ef5df21d9d446b858b3f70a96/watchfiles-1.2.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3", size = 465182, upload-time = "2026-05-18T04:30:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/28/96/716f7e5f51339bf22963f3345f9f27d7f3b30e2eadc597e257c881dd3c53/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0", size = 629841, upload-time = "2026-05-18T04:31:05.397Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/c40783950fd771ccf66ab3ec2722d188a9af1c7f96c6e811f36e40c6e03f/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427", size = 658028, upload-time = "2026-05-18T04:31:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/71/72/4508db1856d1d87fcbb3b63f4839bab1b5682cb0e8d224d122263c09654a/watchfiles-1.2.0-cp313-cp313-win32.whl", hash = "sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799", size = 275183, upload-time = "2026-05-18T04:30:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/f9/36/14b76ca57652e5cc5fd1c11f32a261292c08a0d19a00351013c2549cbfb2/watchfiles-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9", size = 288059, upload-time = "2026-05-18T04:32:07.937Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8d/0a85e395398d8d20fadfe5c5d32c726eee17a519e78fb356f2cf7531bffe/watchfiles-1.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077", size = 280186, upload-time = "2026-05-18T04:31:54.484Z" }, + { url = "https://files.pythonhosted.org/packages/37/68/36db056f1fdcc5f07302f56e631774d6835bcd6fa3ace402304621d5f9e5/watchfiles-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08", size = 399031, upload-time = "2026-05-18T04:30:44.576Z" }, + { url = "https://files.pythonhosted.org/packages/c1/64/01a9d6f66a82a5c101ce939274106cc72759d62427e153f01edd2b9f87c2/watchfiles-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9", size = 391205, upload-time = "2026-05-18T04:30:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/84/2c/0a44fe058cb4bb7b8ede6b6670698bbb7c0400740e378d00022189b7b31d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4", size = 451892, upload-time = "2026-05-18T04:32:14.005Z" }, + { url = "https://files.pythonhosted.org/packages/67/a1/351e0d56cd35e6488b5c8b4fb11a809a5bc923e8fe8fed9faf8920be0c89/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55", size = 458867, upload-time = "2026-05-18T04:31:22.279Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/9d09605187f1b838998624049fcf8bf47b73c1a3b76901fcac1782f62277/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925", size = 490217, upload-time = "2026-05-18T04:31:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/60/5d/a17a16eccb182f04188cd308ec24b1a71a9b5c4e7098269cf35d9fa56d02/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4", size = 571458, upload-time = "2026-05-18T04:32:11.875Z" }, + { url = "https://files.pythonhosted.org/packages/d3/3d/4dd457062083ab1938e5dfd45032eb425cee2ac817287ca8ff4356183e5d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2", size = 464707, upload-time = "2026-05-18T04:30:43.492Z" }, + { url = "https://files.pythonhosted.org/packages/c6/71/ea8c57b128f5383de74d0c7d2d9c57ad7c9a65a930c451bd25d524b295b7/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9", size = 454663, upload-time = "2026-05-18T04:30:16.061Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/2e812bf938406d7db351f0703ddd3fc6c061cf30d96153a77bc79a943a44/watchfiles-1.2.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa", size = 463537, upload-time = "2026-05-18T04:31:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/86/56/d17a7f1dd1bc3035f1072694a551301272f1739c2d8e319c927cb9e29b38/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44", size = 629194, upload-time = "2026-05-18T04:31:14.141Z" }, + { url = "https://files.pythonhosted.org/packages/be/06/f1ff66bf5cae50aa4062779a0ecd0bbaf15e466195719074078947d9a17d/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72", size = 656194, upload-time = "2026-05-18T04:31:47.14Z" }, + { url = "https://files.pythonhosted.org/packages/e7/54/a9c7ea9a82a4ac65e7004c0a03920b5cdd2f9c3b678757d9cd425aa51d53/watchfiles-1.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4", size = 400205, upload-time = "2026-05-18T04:32:05.153Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5d/c9ab3534374a4a67450696905d6ef16a04405448b8dc52bd752ae50423d4/watchfiles-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281", size = 392508, upload-time = "2026-05-18T04:30:54.849Z" }, + { url = "https://files.pythonhosted.org/packages/26/ca/1ad30103535cf0cecd7b993e8d50edc5351b1820e38f2d22e3df58962feb/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d", size = 452448, upload-time = "2026-05-18T04:30:53.727Z" }, + { url = "https://files.pythonhosted.org/packages/37/a1/ceee2cdf2afbd715fa07758d39c9859513eae411b23196f7fd039e5feedd/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e", size = 459605, upload-time = "2026-05-18T04:30:23.312Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f6/421e30fd1cb3907a84ed92ab3f1983e37ba2dca015e9a894a048418417a2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242", size = 490757, upload-time = "2026-05-18T04:30:47.358Z" }, + { url = "https://files.pythonhosted.org/packages/41/b0/55ed1b97ed08be7bba6f9a541cac15f2a858e1d74d2b07b6da70a82aab00/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add", size = 568672, upload-time = "2026-05-18T04:30:38.915Z" }, + { url = "https://files.pythonhosted.org/packages/d1/cf/d8ae8a80dd7bafab395ea7681c10237311bbf34d37704a8c744e7cf31fc7/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f", size = 464197, upload-time = "2026-05-18T04:30:09.914Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8a/3076c496ca8dafe0e8cd03fcebdfc47be4b1174b4e5b24ff6e396e6b3af2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7", size = 453181, upload-time = "2026-05-18T04:30:14.829Z" }, + { url = "https://files.pythonhosted.org/packages/e5/10/9745e17c98e7b8a86454df0a3c7b5686bd650383f1e9f26e4ebcbd6cc0c0/watchfiles-1.2.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e", size = 465109, upload-time = "2026-05-18T04:30:28.123Z" }, + { url = "https://files.pythonhosted.org/packages/8f/95/8ef4a95481d3e0cb52d62a06fa6e972e81424be2d9698b91a2fecca9904c/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06", size = 630653, upload-time = "2026-05-18T04:31:49.304Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e4/3b3bf36b0f829b50c6ebcb8d031583863c59f923d6a6af3d485e470d0fac/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba", size = 657838, upload-time = "2026-05-18T04:31:06.497Z" }, + { url = "https://files.pythonhosted.org/packages/21/b1/6cbbb50c1f3002ab568777d44aa21206dfb8807a840990c4037523b51812/watchfiles-1.2.0-cp314-cp314-win32.whl", hash = "sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7", size = 275108, upload-time = "2026-05-18T04:30:06.891Z" }, + { url = "https://files.pythonhosted.org/packages/92/45/190ce6db8dcb4536682cf75d3889ff1a27182a58cb519d343cb6d9ea63d8/watchfiles-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103", size = 288441, upload-time = "2026-05-18T04:32:12.901Z" }, + { url = "https://files.pythonhosted.org/packages/74/0d/3eae1c2313ab08378431d907c3f8095ecca00f3eda33111cf4f0f2591799/watchfiles-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3", size = 280684, upload-time = "2026-05-18T04:31:26.902Z" }, + { url = "https://files.pythonhosted.org/packages/b1/75/fb64e6c25d6b5ca636d03df34ffb1c6e9873303e76d27967e045f8df088f/watchfiles-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2", size = 398857, upload-time = "2026-05-18T04:32:17.108Z" }, + { url = "https://files.pythonhosted.org/packages/73/4e/9f7adf01754cbf81843722ccfec169d8f26c69778281a302855cecd2ee08/watchfiles-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28", size = 392413, upload-time = "2026-05-18T04:31:07.911Z" }, + { url = "https://files.pythonhosted.org/packages/47/c8/bec626bcc2d69f44b9acb24ce7d60ed7b16b73628eea747fcbd169d8edda/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831", size = 452409, upload-time = "2026-05-18T04:31:20.142Z" }, + { url = "https://files.pythonhosted.org/packages/00/b7/b6362068e81e7c556d155a34c35d40ac3ef42d747b06d7f6e5bf58e359c2/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33", size = 458827, upload-time = "2026-05-18T04:32:06.219Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/9a813fa42afb1e0b4625e75f0479826644d3ee8dc287e093799bc01f390c/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4", size = 490104, upload-time = "2026-05-18T04:31:56.034Z" }, + { url = "https://files.pythonhosted.org/packages/2f/bf/27dfb6094ca4c9aad21298b5525b6c53cb36121ee454331d05161e58d130/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b", size = 571360, upload-time = "2026-05-18T04:31:57.133Z" }, + { url = "https://files.pythonhosted.org/packages/fb/39/44a096d67270ea93df91d33877dbe91fbda3aa4f8ec2edf799d93eda8736/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666", size = 464644, upload-time = "2026-05-18T04:30:57.33Z" }, + { url = "https://files.pythonhosted.org/packages/0e/80/c7472203bad6268e3ef1ad260739704847898938ad7ea8b63a5131f46b50/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925", size = 454771, upload-time = "2026-05-18T04:30:48.736Z" }, + { url = "https://files.pythonhosted.org/packages/51/cf/3b10b268b4b7f0fc26e9debb5eef1998b515887840f444cd3ec80c688755/watchfiles-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b", size = 463494, upload-time = "2026-05-18T04:31:33.826Z" }, + { url = "https://files.pythonhosted.org/packages/3d/3e/a4302545cd589262a0dc7d140e86f7688eba3f9c72776c27f7e23b8864c4/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30", size = 629383, upload-time = "2026-05-18T04:31:15.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/99/d5649df0a9a410d45b7c882304d0b790903ac9b6e8f2cfd12114e0c6b9f2/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5", size = 656093, upload-time = "2026-05-18T04:31:58.707Z" }, + { url = "https://files.pythonhosted.org/packages/92/b9/362702539275019a54dd2e94511b31a9b89c5f9e6a21966de7eb692549fc/watchfiles-1.2.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374", size = 400109, upload-time = "2026-05-18T04:31:16.879Z" }, + { url = "https://files.pythonhosted.org/packages/8f/75/71d5ba62db781e5587bded1d944c675374bc4aa37ff33d5018d98e8b6538/watchfiles-1.2.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65", size = 392167, upload-time = "2026-05-18T04:31:28.058Z" }, + { url = "https://files.pythonhosted.org/packages/3c/01/c66dd95d0423fe30d31820e2d1d5bda773764131bbb6ac0cb1cf303ac328/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69", size = 452372, upload-time = "2026-05-18T04:31:00.836Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/2fe99557e72f85627c6a8eed50d889e8d101623e060a22ad75b875cb932d/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579", size = 459596, upload-time = "2026-05-18T04:31:34.96Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/d4acfa0023367428ed48351b3b9b267893037b6cadae55620c61c24bcfd4/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7", size = 490869, upload-time = "2026-05-18T04:31:59.923Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5f/3164cbdce06c9fb95c4f7b9e2f9760b5e2797af43a9ecc317ef42a23a278/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2", size = 571641, upload-time = "2026-05-18T04:32:00.948Z" }, + { url = "https://files.pythonhosted.org/packages/41/e6/85d3731c55e65cd7690f3f803d24c139588aaf863e4bf2148fe7a7fa1a19/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6", size = 464444, upload-time = "2026-05-18T04:30:34.298Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7d/562641012b8b09872742c3b8adf9629ec479fd78f8d68ae4a0c13da8add6/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4", size = 453593, upload-time = "2026-05-18T04:31:23.464Z" }, + { url = "https://files.pythonhosted.org/packages/56/fe/cb8ef3d6f929d14158fdaaad9925985b7310abc9384dcd4d82dd0016fb59/watchfiles-1.2.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488", size = 465096, upload-time = "2026-05-18T04:31:30.384Z" }, + { url = "https://files.pythonhosted.org/packages/25/91/80908e835e100527a9267147b08c0eee1fa6ab0ffec15edc04d1d44885f7/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb", size = 630638, upload-time = "2026-05-18T04:30:49.89Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/95ab2f256bb4af3cb2eb23b9317bda984ee6e0f11733a5c004a6c95b06e3/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377", size = 657684, upload-time = "2026-05-18T04:31:32.027Z" }, + { url = "https://files.pythonhosted.org/packages/23/f4/7513ef1e85fc4c6331b59479d6d72661fc391fbe543678052ac72c8b6c19/watchfiles-1.2.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4674d49eb94706dfe666c069fc0a1b646ffcf920473492e209f6d5f60d3f0cc2", size = 403050, upload-time = "2026-05-18T04:30:36.753Z" }, + { url = "https://files.pythonhosted.org/packages/27/0b/a54103cfd732bb703c7a749222011a0483ef3705948dae3b203158601119/watchfiles-1.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:094b9b70103d4e963499bdea001ee3c2697b144cd9ae6218a62c0f89ec9e31db", size = 396629, upload-time = "2026-05-18T04:32:03.268Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2c/73f31a3b893886206c3f54d73e8ad8dee58cdb2f69ad2622e0a8a9e07f4e/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0ef001f8c25ad0fa9529f914c1600647ecd0f542d11c19b7894768c67b6acb7", size = 457318, upload-time = "2026-05-18T04:31:01.932Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f9/45d021e4a5cc7b9dd567f7cbb06d3b75f751a690063fb6cc7ec60f4e46b7/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a88fc94e647bc4eec523f1caa540258eb71d14278b9daf72fa1e2658a98df0f0", size = 457771, upload-time = "2026-05-18T04:30:56.331Z" }, +] + +[[package]] +name = "websockets" +version = "16.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/f7/bc3a25c5ec26ce62ce487690becc2f3710bbc7b33338f005ad390db0b986/websockets-16.1.1.tar.gz", hash = "sha256:db234eda965dcce15df96bb9709f587cd87d4d52aaf0e80e2f34ec04c7670c57", size = 182204, upload-time = "2026-07-17T22:51:05.858Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/03/47debfe28e9d6d354be5d777b67fd44c359b9eb299a5d103500bd7cc3e37/websockets-16.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d0fcf657e9f13ff4b177960ab2200237b12994232dfb6df16f1cfe1d4339f93c", size = 179566, upload-time = "2026-07-17T22:48:49.596Z" }, + { url = "https://files.pythonhosted.org/packages/72/93/31efa1ed78c17e5cfc229fd449e3966e1b9cc15753204cd585cc8dd01f4a/websockets-16.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b852788aa51764e2d8e4cf5493d559326bcae5e38d16ba25ffa322b034df272a", size = 177250, upload-time = "2026-07-17T22:48:50.942Z" }, + { url = "https://files.pythonhosted.org/packages/01/4a/542378ab3972b0c1cf1df3df3eff9591cea0d30c58c3aa3c4ddbc244e787/websockets-16.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1427fb4cf0d72f66333e2cacc3ff5f575bf2d7008166ce991a4a470b21d51a22", size = 177528, upload-time = "2026-07-17T22:48:52.59Z" }, + { url = "https://files.pythonhosted.org/packages/33/d9/162321f63c7eed558e9e1798ed7a1e34a4f6dab51f35419e4ed7a4907979/websockets-16.1.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:da4ca1a9d72f9030b3146b8d7022719a9f3d478f61efe6f7dd51d243f61c51b2", size = 186859, upload-time = "2026-07-17T22:48:53.915Z" }, + { url = "https://files.pythonhosted.org/packages/de/09/87df740f7430ce564bd52402e9c9458d4d0459cc7d2ee29e530c8204851b/websockets-16.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86d7f0f8bdb25d2c632b72527325e4776430fd5bc61b9118de4e2b8ddb5f5b01", size = 188095, upload-time = "2026-07-17T22:48:55.384Z" }, + { url = "https://files.pythonhosted.org/packages/d2/12/3d2703af7cc095f3c81904c92208cc1ae79affbc67376944b50ee9301f73/websockets-16.1.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7dfcad78ea1492ee3a9ec765cb7f51bbc17d477107aaf6b22abf7b2558d1c5a0", size = 191385, upload-time = "2026-07-17T22:48:56.742Z" }, + { url = "https://files.pythonhosted.org/packages/1d/69/986aa0234a964a00f5149cfc46e136e96c8faad1c783474550f40d31aef4/websockets-16.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fb9a0a6dc3d1b3986cb88091b6899f0396651e0f74e2c9766ab8d6ffc3842e29", size = 188653, upload-time = "2026-07-17T22:48:58.134Z" }, + { url = "https://files.pythonhosted.org/packages/35/6b/10f9d03e3970a69ba67bd3b46b87a929b586d0300fadbfe14f57c1f85490/websockets-16.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29dfa8114c4a620c69591c5973860f768eac29d3fd6904f37f34266cb219c512", size = 187426, upload-time = "2026-07-17T22:48:59.515Z" }, + { url = "https://files.pythonhosted.org/packages/56/db/bb3aad62bf63d8bb3f0634b2eabffcfb3677a34bd19492110ff6869cf703/websockets-16.1.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ff9417c0ada4d0f7d212f928303e5579bdf3ace4c802fa4afabb30995da58c3", size = 184882, upload-time = "2026-07-17T22:49:00.916Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4c/c09a2ea9bfbeccce52fdc383e5f28af4bc8843338aabac28c81489af6120/websockets-16.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fe0b50da2d84535fb4f7b4bfa951280f97ce3d558a0443b541166d609e67b57", size = 187584, upload-time = "2026-07-17T22:49:02.283Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8b/31bb4eb4d9eaacf1fdd39d115772a8aeaedfc19b5dc262e57ffbc8a9d42c/websockets-16.1.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:34420aaa64440ebd51ac72ca8a45ef4626429438c9b02e633ae412ed43f925d3", size = 186174, upload-time = "2026-07-17T22:49:03.973Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e4/dc02d725610a1ad49e193ef91a548194d71bdc6cdf27da83067dd1f73995/websockets-16.1.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a6a61aff018180c9c50b7b0da33bfd29d378af3497429c95006c589a23a11648", size = 187986, upload-time = "2026-07-17T22:49:05.553Z" }, + { url = "https://files.pythonhosted.org/packages/e0/73/30ed84c8bfd14c73d4af29d5ed9323c3073b48e0b7b23b67070f4e7fd59b/websockets-16.1.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:04fd29a0e2fe9414a95b00e92c67ae51bf900c50c0f8a4b2dafdad621f49ea1d", size = 185565, upload-time = "2026-07-17T22:49:06.959Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d3/4be8d4959f51e31b4f8fc0ece12b45bd3b6c0d15ea23b9990d9c11fc805f/websockets-16.1.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5c31aa7e39ee3e8a358573257f1c0bb5c52430d1b637030dd9c8cc2c282926be", size = 186598, upload-time = "2026-07-17T22:49:08.293Z" }, + { url = "https://files.pythonhosted.org/packages/26/fa/abb38597a52d84ed9cfacadc7a0c6f2db282c0ab23cdf72b58a666a21227/websockets-16.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d14bfb217eb4701e850f1525c9d29d79c44794cdf1c299ead25f39f8c78dea81", size = 186834, upload-time = "2026-07-17T22:49:09.766Z" }, + { url = "https://files.pythonhosted.org/packages/59/80/1119ad08a228b90c4eb77fbe48df7836731a605f5f881ba701ca826a4a65/websockets-16.1.1-cp311-cp311-win32.whl", hash = "sha256:2e28e602bb13da44fbe518c1781a88e3b9d4c3d48d02c9bad83e546164336f57", size = 179940, upload-time = "2026-07-17T22:49:11.196Z" }, + { url = "https://files.pythonhosted.org/packages/71/b2/e511c1c6f64a95c2f3fc54bffda0e14eaa7e9442be605c29270f7589b918/websockets-16.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:7421fad442de870a8cbf2287d1cad7e706ece0dbfeba5e911df132cbdc1cb56a", size = 180239, upload-time = "2026-07-17T22:49:12.519Z" }, + { url = "https://files.pythonhosted.org/packages/17/9d/681cda21c9eee743203a6cb79b9d3d05adad9aa60ec660c6c9bf4dd619ca/websockets-16.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cc97814dfb786a83b6e2dc2e79351e1b83e6d715647d6887fcabd83026417a00", size = 179600, upload-time = "2026-07-17T22:49:13.92Z" }, + { url = "https://files.pythonhosted.org/packages/fb/8d/6195a88b45e8d2a8f745fc2046e36f885a3c9763e6767d2c46229bf9510c/websockets-16.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e047dc87ef7ca50f4d309bf775ad4a71711c58556d75d7bd0604b2317f43e94b", size = 177272, upload-time = "2026-07-17T22:49:15.453Z" }, + { url = "https://files.pythonhosted.org/packages/73/e3/fe2d498c64dea0095c9a9f9a351af4cd6eef31b618395582bc1f38ba45ff/websockets-16.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:01fbdcbac298efe19360b94bc0039c8f746f0220ba570f327577bfee81059175", size = 177542, upload-time = "2026-07-17T22:49:16.875Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ed/f1831681fce0e3242346e5458486003c5f124ed69e5e0b847fd029db4973/websockets-16.1.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f62863e8a00a6d33c3d6566ec0b89f23787b747ffe0c3bc71ec0e76b82c94b1", size = 187137, upload-time = "2026-07-17T22:49:18.323Z" }, + { url = "https://files.pythonhosted.org/packages/6f/79/4ff9dcc1bb46f6b4c536936dde1fd60f9b564f3304307274db97f4c9496d/websockets-16.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8087e82f842609734c9b5a1330464f8e94e346ba0e18c832c08bafa4b0d63c15", size = 188374, upload-time = "2026-07-17T22:49:19.65Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/5c49b6efb36cab733d23773f6de575e1dba65736ead17d5d2b2a1daef779/websockets-16.1.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2bb5d041a8307d2e18782e7ce777f6fdb1e8c2f5d09291484b18c294b789d9aa", size = 191155, upload-time = "2026-07-17T22:49:21.331Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f6/56ccceda3a4838d18f1d40821480da4775397e8b1eecf4031e20c50e2e90/websockets-16.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1db4de4a0e95673f7545d393c49eeb0c2f18ac1ef93073218c79d5cdb2ee75ab", size = 189011, upload-time = "2026-07-17T22:49:22.889Z" }, + { url = "https://files.pythonhosted.org/packages/86/d6/ad5286241a2bce1107e2798d3bfbd62cf79aee167bdb654f8cb1e9dbf949/websockets-16.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f17dbe07eb3ea7f99e4df9b7e0efefe80fbf30d37a8cc4d561a0aed310bc8847", size = 187766, upload-time = "2026-07-17T22:49:24.339Z" }, + { url = "https://files.pythonhosted.org/packages/bc/67/d65c970b7e347fdca69479beb7811c2060529956730a7a4e3ae7c66b0e31/websockets-16.1.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4b57693728576d84ede0a77987ab16881b783d2cd9f1dc180a8fbbc3f79c4428", size = 185173, upload-time = "2026-07-17T22:49:25.743Z" }, + { url = "https://files.pythonhosted.org/packages/1d/5b/14af3cd4ee69d8ea9baca58f3dc3cfb1ba78332a347fd478cb096549d60e/websockets-16.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2a636ff1e7a5c4edf71ef0e79adae7f25dba93b4fcbe3dc958733477ffeb0eaf", size = 187809, upload-time = "2026-07-17T22:49:27.147Z" }, + { url = "https://files.pythonhosted.org/packages/7b/11/be301710d70de97e3e7b3586e6d492c9c06d6a61bf1c2202c36cf0c75607/websockets-16.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d6bec75c290fe484a8ba4cacdf838501e17c06ecfbbf31eede81a9e431bd7751", size = 186412, upload-time = "2026-07-17T22:49:28.611Z" }, + { url = "https://files.pythonhosted.org/packages/db/07/fe1435bf6fe738a3d3b54dbe0c18dabf12cba4d909ac8b58b539ce27c1f4/websockets-16.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:54509b8e92fee4453e152b7558ddef37ce9705a044922f2095a6105e3f80c96f", size = 188290, upload-time = "2026-07-17T22:49:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0a/81f394aff8efcbb01208c1ced77df0a3c7fcce584a88c7273663697946c2/websockets-16.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:f0aa4aad3b1b69ad3fd85a0fd0952ec64331c762bd77ec51cc814170873890b2", size = 185844, upload-time = "2026-07-17T22:49:31.447Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/dd485b995473f415510251fe9bd708f2d24458f439fce958daf8d66dc7c6/websockets-16.1.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:42290eb6db4ccaca7012656738214f8514082fb6fa40cdeb61bb9a471b52e383", size = 186823, upload-time = "2026-07-17T22:49:33.104Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0b/f78de76ff446f1e66af12b43c48a35f31744de93cfdec2f4ea67d5d7bbf1/websockets-16.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:53260c8930da5771cec89439bff99c20c8cb03ddb9588b980697355a83cd4bd3", size = 187102, upload-time = "2026-07-17T22:49:34.616Z" }, + { url = "https://files.pythonhosted.org/packages/37/a1/4cf892007778eaf84ad162bfc98046e0ed89b63ac55949e3236626b2a23f/websockets-16.1.1-cp312-cp312-win32.whl", hash = "sha256:1d27fa8462ad6a1cb36206a3d0640b2333340def181fae11ed7f9adeaa5c0747", size = 179943, upload-time = "2026-07-17T22:49:36.213Z" }, + { url = "https://files.pythonhosted.org/packages/d9/de/6abe251d28c3a3f217096575400b27750b18e0b1d2fff3a2a239960fea07/websockets-16.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:b436f6ec4fc3a6b4237c84d3f83170ed2b40bb584222f0ac47a0c8a5921980c7", size = 180243, upload-time = "2026-07-17T22:49:37.626Z" }, + { url = "https://files.pythonhosted.org/packages/ce/fd/6ec6c6d2850aea25b1b2aa9901a016980bb87d01e89b3eb00470b1b5d471/websockets-16.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ab59169ace05dcb49a1d4118f0bde139557adf45091bd85747e36bf5de984dd1", size = 179587, upload-time = "2026-07-17T22:49:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d8/1d299d2dd34087db39831a34cc645ef8a6f89d78efada6983093513cd81c/websockets-16.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5e3b7d601f6f84156b08cc4a5e541c2b50ad7b36cfc302b657a12477c904a5df", size = 177272, upload-time = "2026-07-17T22:49:40.293Z" }, + { url = "https://files.pythonhosted.org/packages/3d/86/0a70d3ae2f0f2256bb41302d9804dbca65d4360281e7feb3e1f94102ac46/websockets-16.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cd2ca96a082a36964aca83e992f72abeb61b7306c1a6cba4c7d06a7b93750cac", size = 177530, upload-time = "2026-07-17T22:49:41.786Z" }, + { url = "https://files.pythonhosted.org/packages/b5/c2/c676c69444d9db448b3f0a55a98dcc534affce0bce961d9d2f0b8499b10a/websockets-16.1.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f5d497865f05bb222cab7016c6034542e84e5f29f49c6fd3f4939cda7197b5b8", size = 187197, upload-time = "2026-07-17T22:49:43.658Z" }, + { url = "https://files.pythonhosted.org/packages/0b/13/88137fbaf726ebe29d62c1117fa11fa2bbb6209dc79d4ad738efbe36a2aa/websockets-16.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bae954c382e013d5ea5b190d2830526bfa45ad121c326da0049b8c769f185db6", size = 188433, upload-time = "2026-07-17T22:49:45.147Z" }, + { url = "https://files.pythonhosted.org/packages/01/6d/46c2f2ce6751cb26f39293e1ecbf8544cb01321397cd476c2756b98c216d/websockets-16.1.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e09f753a169951eb4f28c2c774f71069304f66e7277e0f5a2892423599cfa854", size = 189868, upload-time = "2026-07-17T22:49:46.581Z" }, + { url = "https://files.pythonhosted.org/packages/29/2b/170a9e8097636cfde4dc3c592b6e00b18a44a2f5407606d96ca542dd5838/websockets-16.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:024193f8551a2b0eafbdd160911012c4e6c228c28430c84433253299a9e42d6a", size = 189059, upload-time = "2026-07-17T22:49:47.972Z" }, + { url = "https://files.pythonhosted.org/packages/a7/48/f0d4ebc9ab4b473b8861b9e20fdb663d515d42f7befdf62cdb60fee7a1ec/websockets-16.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:aabe464bfd13bd25f4821faf111da6fefdc389f870265a53105580e45b0a2e49", size = 187814, upload-time = "2026-07-17T22:49:49.344Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ba/39a41d3ae8e72696a9492581900611c5a91e2b07563b0bcd2523adea9854/websockets-16.1.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a28fcbc9b6baf54a2e23f8655f308e4ccc6afdd7266f8fe7954f320dcda0f785", size = 185229, upload-time = "2026-07-17T22:49:50.787Z" }, + { url = "https://files.pythonhosted.org/packages/3c/36/ac15b604f850d1907f0a85ed721cefe47cd45034b3620069b829746cccbe/websockets-16.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79eace538c6a97e96d0d03d4f9d314f9677f5ed85a8a984992ffd90b13cb8a56", size = 187874, upload-time = "2026-07-17T22:49:52.228Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f3/3fbd5d71d59299c3770faa5884d4f45070236ca5a35ab3a61830812c409a/websockets-16.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:496af849a472b531f758dbd4d61338f5000538cb1a7b3d20d9d32a264517f509", size = 186469, upload-time = "2026-07-17T22:49:53.776Z" }, + { url = "https://files.pythonhosted.org/packages/b4/fc/dd90349bba58af2a53ef2ddd9c32716c81eb6d59a0687939fff561860878/websockets-16.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5283810d2646741a0d8da2aa733d6aefa0545809afccb2a5d105a26bc45125f1", size = 188347, upload-time = "2026-07-17T22:49:55.202Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f3/f73ba86427682da59b78c11d77ba56d5b801c32e84afe79b274bbd6a9bb2/websockets-16.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4e3b680b1e0a27457e727a0d572fd81dffa87b6dbf8b228ab57da64f7d85aead", size = 185903, upload-time = "2026-07-17T22:49:56.75Z" }, + { url = "https://files.pythonhosted.org/packages/34/7c/f95eb20e80104173b3a0a092291f89ea4047ef6e608e0a57ca06eb14eecb/websockets-16.1.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:69159730a823dde3ea8d08783e8d47ef135a6d7e8d44eb127e32b321c9db8e3e", size = 186855, upload-time = "2026-07-17T22:49:58.467Z" }, + { url = "https://files.pythonhosted.org/packages/b0/35/dd875b3e050ff232d60fa377707f890e369f74d134f1be32e8f68879747c/websockets-16.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ed5bb271084b46530ee2ddc0410537a9961152c5ccba2fc98c5276d992ccba87", size = 187140, upload-time = "2026-07-17T22:50:00.016Z" }, + { url = "https://files.pythonhosted.org/packages/e8/dc/5cbfcb41824502f6af93b8f3943a4d06c67c23c7d2e31eb18748c4a5b2a7/websockets-16.1.1-cp313-cp313-win32.whl", hash = "sha256:cfb70b4eb56cac4da0a83588f3ad50d46beb0690391082f3d4e2d488c70b68ea", size = 179928, upload-time = "2026-07-17T22:50:01.685Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c1/71e5deb5b7f8f226997ab64908c184ac3105c0155ce2d486f318e5dd08a8/websockets-16.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:d9531d9cbeac99af6f038fb1bc351403531f7d634a2c2e10e2f7c854c6ed5b68", size = 180242, upload-time = "2026-07-17T22:50:03.117Z" }, + { url = "https://files.pythonhosted.org/packages/73/a2/ba78a164eeea4620df4a4df4bd2ed6017438c4655cc0f36f2c0bc0432355/websockets-16.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:443aefe96b7fdb132e2a70806cca1f2af49bb3f28e47abcd7c2e9dcf4d8fa1b8", size = 179635, upload-time = "2026-07-17T22:50:05.001Z" }, + { url = "https://files.pythonhosted.org/packages/b9/08/d26d7a7628cd4ac34cbbdb63ac80914ca842ed8e42938c40a53567806df3/websockets-16.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6456ff333092d509127d75a638cb411afae8ff17f092635015d1902efec8a293", size = 177320, upload-time = "2026-07-17T22:50:06.427Z" }, + { url = "https://files.pythonhosted.org/packages/0f/45/ebec83e6269536aa5932533c67b0af5c781f3e73fdbcd68672dcf43f4f44/websockets-16.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fce6c48559c86d1ac3632ccb1bebc7d5442fbe79bd9bb0e40379ee54be2a4051", size = 177544, upload-time = "2026-07-17T22:50:07.834Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d5/abc614d2297f6c1c3e01e61260364457a47c25cc1cf6a879038902bc6aa8/websockets-16.1.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:92b820d345f7a3fc7b8163949ee92df910f290c3fc517b3d5301c78065adafe1", size = 187270, upload-time = "2026-07-17T22:50:09.275Z" }, + { url = "https://files.pythonhosted.org/packages/52/71/4c99af3b87dff1b2927981f6876607d4acb45338c665242168d3982f7758/websockets-16.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a606d9c24035242a3e256e9d5b77ed9cd6bccfcb7cf993e5ca3c0f6f68fb6a7", size = 188509, upload-time = "2026-07-17T22:50:10.722Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b4/5c8ca14b0df7eb84ed0524165c5359150210140817a3312aee57bf62a1cf/websockets-16.1.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:414e596c75f74e0994084694189d7dc9229fb278e33064d6784b73ffbba3ca31", size = 189882, upload-time = "2026-07-17T22:50:12.293Z" }, + { url = "https://files.pythonhosted.org/packages/25/c1/bedfba9e70557129cb8083748d167bdcc01483dedf0f0df143676df05cbe/websockets-16.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:536676848fc5961aca9d20389951f59169508f765637a172403dc5434d722fa0", size = 189114, upload-time = "2026-07-17T22:50:13.789Z" }, + { url = "https://files.pythonhosted.org/packages/df/09/aa835b2787835aebd839114be5de51b797cb480b63ba42b26d34dfe147cb/websockets-16.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:97fd3a0e8b53efa41970ac1dff3d8cf0d2884cadeb4caaf95db7ad1526926ee3", size = 187861, upload-time = "2026-07-17T22:50:15.179Z" }, + { url = "https://files.pythonhosted.org/packages/20/26/f6408330694dbc9830857d9d23bc14ac4f6875127a480cfdda8d5ca21198/websockets-16.1.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7b1b19636af86a3c7995d4d028dbe376f39b4bf31541146f9c123582a6c94562", size = 185286, upload-time = "2026-07-17T22:50:16.741Z" }, + { url = "https://files.pythonhosted.org/packages/17/9a/e0675e70dd8a80762cf35bb18799d3f290a4890ffe6439bc51d222796083/websockets-16.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:41c8e77f17294c0ac18008a7309b99b34ee72247ef10b6dff4c3f8b5ac29896b", size = 187935, upload-time = "2026-07-17T22:50:18.213Z" }, + { url = "https://files.pythonhosted.org/packages/33/c1/3234cfb86afde01b81e9bddcc6e534c440975d60a13991259e833069ab3e/websockets-16.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9f63bcef7f4b02b06b35fc01c93b96c43b5e88e1e8868676caacf493d5a31f3a", size = 186444, upload-time = "2026-07-17T22:50:19.67Z" }, + { url = "https://files.pythonhosted.org/packages/89/87/9c15206e1d778923d8daa9657de07aa62ea815e13448319c98458c37b281/websockets-16.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dab9eb87869da2d6ed3af3f3adf28414baae6ec9d4df355ffc18889132f3436c", size = 188409, upload-time = "2026-07-17T22:50:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/cf5de5c67676de2d3eef8b2a518f168f6796595447a5b7161ba0d012915c/websockets-16.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:43e3a9fdd7cbf7ba6040c31fae0faf84ca1474fef777c4e37912f1540f854499", size = 185958, upload-time = "2026-07-17T22:50:22.719Z" }, + { url = "https://files.pythonhosted.org/packages/62/c0/731b6ddede2e4136912ec4cff2cffbda35af73546be4762c3d7bd3bd79af/websockets-16.1.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:056ae37939ed7e9974f364f5864e76e49182622d8f9751ac1903c0d09b013985", size = 186911, upload-time = "2026-07-17T22:50:24.108Z" }, + { url = "https://files.pythonhosted.org/packages/8c/7f/39c634472c4469a24a7c09cecddffb08fac6d0e74f73881a94ee8a40a196/websockets-16.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a0eadbbf2c30f01efa58e1f110eb6fa293261f6b0b1aa38f7f48707107690af9", size = 187204, upload-time = "2026-07-17T22:50:25.548Z" }, + { url = "https://files.pythonhosted.org/packages/26/89/9667c256c256dafcc62d21328ce7a40067da857969b68ee9af375b0aaf72/websockets-16.1.1-cp314-cp314-win32.whl", hash = "sha256:195c978b065fa40910582464f99d6b15c8b314c68e0546549a55ed83f4735328", size = 179603, upload-time = "2026-07-17T22:50:27.086Z" }, + { url = "https://files.pythonhosted.org/packages/bd/dd/1c099d6c0fc5deb6b46ccdbb6981fdb4b12c917869cb3952408409dc18db/websockets-16.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:4e8d01cc3bcae7bbf8167f944aeafefed590fae5693552bba9794a9df68371cc", size = 179948, upload-time = "2026-07-17T22:50:28.521Z" }, + { url = "https://files.pythonhosted.org/packages/35/25/9956b2d5e0529d5d23924f21bba1440d4c5c88a562e4f08550871ffa97a7/websockets-16.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0ffd3031ea8bda8d61762e84220186105ba3b748b3c8da2ae4f7816fac03e573", size = 179963, upload-time = "2026-07-17T22:50:29.982Z" }, + { url = "https://files.pythonhosted.org/packages/17/06/55ffc976c488b6aee9ea05761ff7c4e88e7c1fd82818c8ca7b556ad2f90c/websockets-16.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:84a2cef8deffbd9ab8ee0ea546a2a6a7030c28f44e6cdd4547dbfeb489eb8999", size = 177497, upload-time = "2026-07-17T22:50:31.396Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/f7dac2e980bacc92bdc26cebae4ae4d50cae5380732c50980598fc0bbae4/websockets-16.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3df13f73af9b3b38ab1195eb299ecb67a4330c911c97ae04043ff74085728abe", size = 177698, upload-time = "2026-07-17T22:50:32.829Z" }, + { url = "https://files.pythonhosted.org/packages/b2/39/26762f734113e22da2b942c3aca85798e0c0405d64c256549540ff31e5a1/websockets-16.1.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:23253dd5bcae3f9aaee0a1d30967a8dbd52e5d3cff93a2e5b84df57b77d4750d", size = 187561, upload-time = "2026-07-17T22:50:34.24Z" }, + { url = "https://files.pythonhosted.org/packages/11/94/c3f330851806b9b02138b774d593478323e73c99238681b4b93efe64e02d/websockets-16.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1c5705e314449e3308872fe084b8571ce078ee4fc55a98a769bdefe5917392", size = 188732, upload-time = "2026-07-17T22:50:36.088Z" }, + { url = "https://files.pythonhosted.org/packages/d1/f2/eb2c450f052de334ae33cf200ece6e87b0e14d186807074e4eb1cd2cdea2/websockets-16.1.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69e52d175a0a7d1e13b4b67ad41c560b7d98e8c6f6126eb0bda496c784faf8c7", size = 190872, upload-time = "2026-07-17T22:50:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/70/31/2ac8cecf3a74f7fed9132129fc3d90b3998a1554570c11a69b2a8c20332d/websockets-16.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1f79c89b5eb034d1722938a891916582f8f7f503f58ca22518a63c3f2cd18499", size = 189305, upload-time = "2026-07-17T22:50:39.53Z" }, + { url = "https://files.pythonhosted.org/packages/6a/cf/8ab19650d3c0d4562c92e70ab47c257c4aa5c6a713ed87fe63766b31fefc/websockets-16.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:39f2a024af5c345ffe8fcf1ee18c049c024c94df393bb09b044a6917c77bde43", size = 188033, upload-time = "2026-07-17T22:50:40.912Z" }, + { url = "https://files.pythonhosted.org/packages/66/d7/a49a38a6127a4acb134fb1912b215d900cc657605cff32445bf519f3acc4/websockets-16.1.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:952303a7318d4cbe1011400839bb2051c9f84fa0a35923267f5daba34b15d458", size = 185748, upload-time = "2026-07-17T22:50:42.559Z" }, + { url = "https://files.pythonhosted.org/packages/95/3e/ad1fa40388c7f2e0bb2c7930d0090b6c5498594bd1cdaec18864df3d9e97/websockets-16.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:249116b4a76063d930a46391ad56e135c286e4562a18309029fc2c73f4ed4c62", size = 188285, upload-time = "2026-07-17T22:50:43.974Z" }, + { url = "https://files.pythonhosted.org/packages/35/b8/d5db28ca264b9104f82196f92dc8843e35fd391f763d42e4ad358f5bc97e/websockets-16.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:61922544a0587a13fd3f53e4c0e5e606510c7b0d9d22c8444e5fae22a06b38cb", size = 186777, upload-time = "2026-07-17T22:50:45.474Z" }, + { url = "https://files.pythonhosted.org/packages/42/9c/726cb39d0cc43ae848dce4aa2acb04eecc6738b1264ec6d700bf6bcfb9f8/websockets-16.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:46dcaa042cd1de6c59e7d9269fa63ff7572b6df40510600b678f0826b3c7af51", size = 188682, upload-time = "2026-07-17T22:50:46.973Z" }, + { url = "https://files.pythonhosted.org/packages/be/c7/1168704de8c2dd483edabe4a22cbe4465dd8be8dd95561d214f9fe092871/websockets-16.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:38565aca3e01ea8734e578fb2118dade0ecb0250533f29e22b8d1a7a196cf4d0", size = 186377, upload-time = "2026-07-17T22:50:48.413Z" }, + { url = "https://files.pythonhosted.org/packages/ca/40/f9ff2d630ffce4e7dfea0b2288e1caf9ebbf9ff8a9ec9396136ce8b94935/websockets-16.1.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:42f599f4d48c7e1a3338fdaac3acd075be3b3cf02d4b274f3bf2767aedd3d217", size = 187148, upload-time = "2026-07-17T22:50:49.845Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/e177c8299f78d7cbe2d14df228643c10c70c0e86e108e092056bbcc16e46/websockets-16.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dcc04fedf83effaeb9cce98abc9469bb1b42ef85f03e01c8c1f4438ef7555737", size = 187578, upload-time = "2026-07-17T22:50:51.619Z" }, + { url = "https://files.pythonhosted.org/packages/49/b2/b6987faf330f5af5c787a2610124c2e8403d51724f9001ec4fff6311fe7a/websockets-16.1.1-cp314-cp314t-win32.whl", hash = "sha256:8483c2096363120eea8b07c06ae7304d520f686665fffd4811fad423930a65d7", size = 179729, upload-time = "2026-07-17T22:50:53.269Z" }, + { url = "https://files.pythonhosted.org/packages/a2/6e/fbac6ed878dd362fbad7d415fa4f84d38e3e33fed8cde45c64e783acf826/websockets-16.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:bcce07e23e5769375158f5efdcdafa8d5cd014b93c6683865b840ed65b96f231", size = 180072, upload-time = "2026-07-17T22:50:54.969Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ed/71fea6e141590cafc40b14dc5943b0845606bee87bdb52a21b6a73eb4311/websockets-16.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:820fb8450edddae3812fd58cbc08e2bf22812cb248ecb5f06dbb82119a56e869", size = 177185, upload-time = "2026-07-17T22:50:56.665Z" }, + { url = "https://files.pythonhosted.org/packages/01/ec/00e7eeca200facf9266a83e4cbbf1bed0e67fba1d4d45031d3e5b3d81b5c/websockets-16.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:125f22dbefaf1554fea66fc83851490edb284ce4f501d37ffed2752f418332d9", size = 177459, upload-time = "2026-07-17T22:50:58.197Z" }, + { url = "https://files.pythonhosted.org/packages/75/fd/5774c4b33f7c0d8f0c51809c8b3a93456c48e3543579262cfa64eb5f522e/websockets-16.1.1-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:30bbe120437b5648a77d3519b7024ea09530e0b5b18d3698c5a0ae536fe0cc2e", size = 178294, upload-time = "2026-07-17T22:50:59.641Z" }, + { url = "https://files.pythonhosted.org/packages/37/c3/48e2c03d2bd79bb45948841c592d24156312dd5f58cdf8f549febe652fb6/websockets-16.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b6b9dadbef0cccd9f4c4ee96b08898afa73e26803bbe0f6aeb5bb12b0074206d", size = 179190, upload-time = "2026-07-17T22:51:01.129Z" }, + { url = "https://files.pythonhosted.org/packages/2d/3f/73e511ecf2496ceac57dd4ed8388efe2bcf0769338a2dbf242c8366ae87e/websockets-16.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56cd5fc4f10a9ea8aa0804bddb7b42506cf9e136046f3b4c27de8fec9e2ecba5", size = 180330, upload-time = "2026-07-17T22:51:02.603Z" }, + { url = "https://files.pythonhosted.org/packages/be/4d/2d0d67834092e354d2b0498f014a41249a89556bc406cf86f3e1557bb463/websockets-16.1.1-py3-none-any.whl", hash = "sha256:6abbd3e82c731c8e531714466acd5d87b5e88ac3243465337ba71d68e23ae7e3", size = 173814, upload-time = "2026-07-17T22:51:04.184Z" }, +] diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index e83f284..83d4749 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1,11 +1,15 @@ import { Navigate, Route, Routes } from 'react-router-dom'; import { WorkspacePage } from './pages/WorkspacePage'; import { EditorPage } from './pages/EditorPage'; +import { ChartEnginePage } from './pages/ChartEnginePage'; +import { ChartGenerationPage } from './pages/ChartGenerationPage'; export function App() { return ( } /> + } /> + } /> } /> } /> diff --git a/apps/web/src/api/client.ts b/apps/web/src/api/client.ts index 94b7314..a10bcd2 100644 --- a/apps/web/src/api/client.ts +++ b/apps/web/src/api/client.ts @@ -8,10 +8,17 @@ import type { AnalyzeResponse, ApiErrorBody, CandidateEvent, + ChartCorpusStatistics, + ChartDocument, + ChartGenerationResponse, + ChartMode, + GenerateChartRequest, HitPoint, Project, ProjectDetail, ProjectListResponse, + ReferenceChartGroup, + ReferenceChartListResponse, LyricsInputFormat, StemKind, TempoSegment, @@ -68,6 +75,34 @@ async function request(path: string, init: RequestInit = {}): Promise { } export const api = { + getReferenceCharts: (filters: { + mode?: ChartMode; + group?: ReferenceChartGroup | ''; + search?: string; + } = {}) => { + const params = new URLSearchParams(); + if (filters.mode) params.set('mode', filters.mode); + if (filters.group) params.set('group', filters.group); + if (filters.search) params.set('search', filters.search); + const query = params.size ? `?${params}` : ''; + return request(`/chart-engine/reference-charts${query}`); + }, + + getReferenceChart: (chartId: string) => + request(`/chart-engine/reference-charts/${encodeURIComponent(chartId)}`), + + getChartCorpusStatistics: () => + request('/chart-engine/statistics'), + + generateChart: (trackId: string, input: GenerateChartRequest) => + request(`/tracks/${encodeURIComponent(trackId)}/chart/generate`, { + method: 'POST', + body: JSON.stringify(input), + }), + + getLatestChart: (trackId: string) => + request(`/tracks/${encodeURIComponent(trackId)}/chart/latest`), + getAlignmentMethods: () => request('/alignment/methods'), runAlignment: (trackId: string, method: AlignmentMethodId) => @@ -190,6 +225,14 @@ export const api = { }), audioUrl: (trackId: string) => apiUrl(`/tracks/${encodeURIComponent(trackId)}/audio`), + referenceChartAudioUrl: (chartId: string) => + apiUrl(`/chart-engine/reference-charts/${encodeURIComponent(chartId)}/audio`), + chartExportUrl: (trackId: string, generationId?: string) => { + const params = new URLSearchParams(); + if (generationId) params.set('generationId', generationId); + const query = params.size ? `?${params}` : ''; + return apiUrl(`/tracks/${encodeURIComponent(trackId)}/chart/export${query}`); + }, exportUrl: ( trackId: string, format: 'json' | 'csv' | 'package', diff --git a/apps/web/src/components/ChartPlayer.tsx b/apps/web/src/components/ChartPlayer.tsx new file mode 100644 index 0000000..b2a2c4c --- /dev/null +++ b/apps/web/src/components/ChartPlayer.tsx @@ -0,0 +1,117 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import type { ChartDocument } from '../types'; +import { + clampPlaybackTime, + DEFAULT_CHART_SCROLL_SPEED, + type ChartScrollSpeed, +} from '../utils/chartPreview'; +import { ChartTransport } from './ChartTransport'; +import { FiveLaneChartPreview } from './FiveLaneChartPreview'; + +interface ChartPlayerProps { + chart: ChartDocument; + audioUrl: string; + durationSec?: number; +} + +export function ChartPlayer({ chart, audioUrl, durationSec = chart.durationSec }: ChartPlayerProps) { + const audioRef = useRef(null); + const [currentTimeSec, setCurrentTimeSec] = useState(0); + const [mediaDurationSec, setMediaDurationSec] = useState(durationSec); + const [playing, setPlaying] = useState(false); + const [audioError, setAudioError] = useState(''); + const [scrollSpeed, setScrollSpeed] = useState(DEFAULT_CHART_SCROLL_SPEED); + + useEffect(() => { + setPlaying(false); + setCurrentTimeSec(0); + setMediaDurationSec(durationSec); + setAudioError(''); + }, [audioUrl, durationSec]); + + useEffect(() => { + if (!playing) return; + let frame = 0; + const update = () => { + const audio = audioRef.current; + if (!audio) return; + setCurrentTimeSec(audio.currentTime); + frame = requestAnimationFrame(update); + }; + frame = requestAnimationFrame(update); + return () => cancelAnimationFrame(frame); + }, [playing]); + + const togglePlay = useCallback(() => { + const audio = audioRef.current; + if (!audio) return; + if (audio.paused) { + setAudioError(''); + void audio.play().catch((error: unknown) => { + setPlaying(false); + const detail = error instanceof Error && error.message ? `:${error.message}` : ''; + setAudioError(`无法播放参考音频${detail}`); + }); + } else { + audio.pause(); + } + }, []); + + const seek = useCallback((nextTimeSec: number) => { + const audio = audioRef.current; + const next = clampPlaybackTime(nextTimeSec, mediaDurationSec); + if (audio) audio.currentTime = next; + setCurrentTimeSec(next); + }, [mediaDurationSec]); + + const seekBy = useCallback((deltaSeconds: number) => { + const sourceTime = audioRef.current?.currentTime ?? currentTimeSec; + seek(sourceTime + deltaSeconds); + }, [currentTimeSec, seek]); + + return ( +
+
+ ); +} diff --git a/apps/web/src/components/ChartTransport.tsx b/apps/web/src/components/ChartTransport.tsx new file mode 100644 index 0000000..5bf29dc --- /dev/null +++ b/apps/web/src/components/ChartTransport.tsx @@ -0,0 +1,91 @@ +import { formatTime } from '../utils/time'; +import { + CHART_SCROLL_SPEED_OPTIONS, + isChartScrollSpeed, + type ChartScrollSpeed, +} from '../utils/chartPreview'; + +interface ChartTransportProps { + currentTimeSec: number; + durationSec: number; + playing: boolean; + scrollSpeed: ChartScrollSpeed; + disabled?: boolean; + onTogglePlay: () => void; + onSeek: (timeSec: number) => void; + onSeekBy: (deltaSeconds: number) => void; + onScrollSpeedChange: (scrollSpeed: ChartScrollSpeed) => void; +} + +export function ChartTransport({ + currentTimeSec, + durationSec, + playing, + scrollSpeed, + disabled = false, + onTogglePlay, + onSeek, + onSeekBy, + onScrollSpeedChange, +}: ChartTransportProps) { + const safeDuration = Math.max(0, durationSec); + const safeCurrent = Math.max(0, Math.min(safeDuration, currentTimeSec)); + return ( +
+ + + +
+ {formatTime(safeCurrent)} + / {formatTime(safeDuration)} +
+ + +
+ ); +} diff --git a/apps/web/src/components/ChartValidationPanel.tsx b/apps/web/src/components/ChartValidationPanel.tsx new file mode 100644 index 0000000..44edbf2 --- /dev/null +++ b/apps/web/src/components/ChartValidationPanel.tsx @@ -0,0 +1,49 @@ +import type { ChartDocument } from '../types'; + +export function ChartValidationPanel({ chart }: { chart: ChartDocument | null }) { + const validation = chart?.validation ?? null; + const statistics = chart?.statistics ?? null; + const fullStepReachable = validation?.metrics.fullStepReachable; + return ( +
+
+ PLAYABILITY VALIDATOR +

人体可玩性

+
+ {validation ? ( + <> +
+ {validation.score.toFixed(0)} + {validation.valid ? '通过验证' : '需要修正'}满分 100 +
+
+ AVG NPS{statistics?.npsAverage.toFixed(2) ?? '—'} + PEAK NPS{statistics?.npsPeak.toFixed(2) ?? '—'} + JUMPS{statistics?.jumpCount ?? 0} + HOLDS{statistics?.holdCount ?? 0} +
+ {typeof fullStepReachable === 'boolean' ? ( +
+ FULL STEP · 全步伐严格左右交替 · 无转圈 + {fullStepReachable ? '通过' : '无解'} +
+ ) : null} + {validation.issues.length ? ( +
    + {validation.issues.map((issue, index) => ( +
  1. + {issue.severity} + {issue.code.replaceAll('_', ' ')} +

    {issue.message}

    + {issue.timeSec !== null ? {issue.timeSec.toFixed(3)} s · beat {issue.beat?.toFixed(3) ?? '—'} : null} +
  2. + ))} +
+ ) :
没有发现密度、移动、同脚或非全步伐风险。
} + + ) : ( +
生成谱面后显示 Validator 分数和问题定位。
+ )} +
+ ); +} diff --git a/apps/web/src/components/EditorToolbar.tsx b/apps/web/src/components/EditorToolbar.tsx index 1dc50d5..aae9f17 100644 --- a/apps/web/src/components/EditorToolbar.tsx +++ b/apps/web/src/components/EditorToolbar.tsx @@ -104,6 +104,7 @@ export function EditorToolbar({ project, track, mode, sensitivity, onModeChange,
+ AI Chart
diff --git a/apps/web/src/pages/ChartEnginePage.tsx b/apps/web/src/pages/ChartEnginePage.tsx new file mode 100644 index 0000000..2890523 --- /dev/null +++ b/apps/web/src/pages/ChartEnginePage.tsx @@ -0,0 +1,142 @@ +import { useMemo, useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { Link, useSearchParams } from 'react-router-dom'; +import { api } from '../api/client'; +import { Brand } from '../components/Brand'; +import { ChartPlayer } from '../components/ChartPlayer'; +import type { ReferenceChartGroup } from '../types'; +import { formatTime } from '../utils/time'; + +const GROUPS: Array<{ value: '' | ReferenceChartGroup; label: string }> = [ + { value: '', label: '全部' }, + { value: 'SPEED_CLUB', label: 'CLUB' }, + { value: 'SPEED_DEVIL', label: 'DEVIL' }, + { value: 'SPEED_REMIX', label: 'REMIX' }, +]; + +export function ChartEnginePage() { + const [searchParams, setSearchParams] = useSearchParams(); + const [search, setSearch] = useState(''); + const [group, setGroup] = useState<'' | ReferenceChartGroup>(''); + const listQuery = useQuery({ + queryKey: ['reference-charts', 'pump-single', group, search.trim()], + queryFn: () => api.getReferenceCharts({ mode: 'pump-single', group, search: search.trim() }), + }); + const statisticsQuery = useQuery({ + queryKey: ['chart-corpus-statistics'], + queryFn: api.getChartCorpusStatistics, + staleTime: Infinity, + }); + const selectedFromUrl = searchParams.get('chart'); + const selectedChartId = selectedFromUrl || listQuery.data?.items[0]?.id || ''; + const chartQuery = useQuery({ + queryKey: ['reference-chart', selectedChartId], + queryFn: () => api.getReferenceChart(selectedChartId), + enabled: Boolean(selectedChartId), + staleTime: Infinity, + }); + const selectedSummary = useMemo( + () => listQuery.data?.items.find((item) => item.id === selectedChartId), + [listQuery.data?.items, selectedChartId], + ); + const chart = chartQuery.data; + + const chooseChart = (chartId: string) => { + const next = new URLSearchParams(searchParams); + next.set('chart', chartId); + setSearchParams(next); + }; + + return ( +
+
+
+ +
+
REAL SPEED CORPUS

AI Chart Engine

+
+ +
+ +
+
ALL CHARTS{statisticsQuery.data?.chartCount ?? listQuery.data?.corpusTotal ?? '—'}
+
FIVE-LANE{statisticsQuery.data?.singleChartCount ?? listQuery.data?.total ?? '—'}
+
SONGS{statisticsQuery.data?.songCount ?? '—'}
+
TOTAL NOTES{statisticsQuery.data?.totalNotes.toLocaleString() ?? '—'}
+
GROUPSCLUB · DEVIL · REMIX
+
+ +
+ + +
+ {chartQuery.isLoading ?
正在解析 SM 与绝对时间…
: null} + {chartQuery.isError ?
无法载入谱面

{chartQuery.error.message}

: null} + {chart ? ( + <> +
+
{chart.sourceGroup} / {chart.mode}

{chart.title}

{chart.artist || '未知艺术家'} · {chart.music}

+
{chart.difficulty}Lv.{chart.meter}
+
+
+ BPM{chart.bpm.toFixed(chart.bpm % 1 ? 1 : 0)} + OFFSET{chart.offsetSec >= 0 ? '+' : ''}{chart.offsetSec.toFixed(3)} s + TEMPO MAP{chart.tempoMap.length} 段 + DURATION{formatTime(chart.durationSec).slice(0, -4)} + EVENTS{chart.statistics?.eventCount.toLocaleString() ?? chart.events.length.toLocaleString()} + NPS PEAK{chart.statistics?.npsPeak.toFixed(2) ?? '—'} +
+ + + ) : !chartQuery.isLoading && !chartQuery.isError ? ( +
选择一首真实谱面开始预览

播放器会以解析后的绝对 timeSec 同步原始 MP3。

+ ) : null} +
+
+
+ ); +} diff --git a/apps/web/src/pages/ChartGenerationPage.tsx b/apps/web/src/pages/ChartGenerationPage.tsx new file mode 100644 index 0000000..2b681ca --- /dev/null +++ b/apps/web/src/pages/ChartGenerationPage.tsx @@ -0,0 +1,154 @@ +import { useEffect, useRef, useState } from 'react'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { Link, useParams } from 'react-router-dom'; +import { api, ApiError } from '../api/client'; +import { Brand } from '../components/Brand'; +import { ChartPlayer } from '../components/ChartPlayer'; +import { ChartValidationPanel } from '../components/ChartValidationPanel'; +import type { ChartDocument } from '../types'; +import { createGenerationSeedSequence } from '../utils/generationSeed'; +import { formatTime } from '../utils/time'; + +const DIFFICULTY_LEVELS = Array.from({ length: 15 }, (_, index) => index + 1); + +export function ChartGenerationPage() { + const { projectId = '' } = useParams(); + const queryClient = useQueryClient(); + const [difficulty, setDifficulty] = useState(8); + const difficultyInitialized = useRef(false); + const [enableSpin, setEnableSpin] = useState(false); + const [generatedChart, setGeneratedChart] = useState(null); + const [takeGenerationSeed] = useState<() => number>(() => createGenerationSeedSequence()); + const projectQuery = useQuery({ + queryKey: ['project', projectId], + queryFn: () => api.getProject(projectId), + enabled: Boolean(projectId), + }); + const track = projectQuery.data?.track ?? null; + const latestChartQuery = useQuery({ + queryKey: ['latest-chart', track?.id], + queryFn: () => api.getLatestChart(track!.id), + enabled: Boolean(track?.id), + retry: false, + }); + useEffect(() => { + if (!difficultyInitialized.current && latestChartQuery.data) { + setDifficulty(latestChartQuery.data.meter); + difficultyInitialized.current = true; + } + }, [latestChartQuery.data]); + const generateMutation = useMutation({ + mutationFn: () => api.generateChart(track!.id, { + difficulty, + enableSpin, + useLocalModel: true, + seed: takeGenerationSeed(), + }), + onSuccess: (response) => { + setGeneratedChart(response.chart); + queryClient.setQueryData(['latest-chart', track?.id], response.chart); + }, + }); + const chart = generatedChart ?? latestChartQuery.data ?? null; + const project = projectQuery.data; + const analysisReady = Boolean(track?.tempoMap.length && (track.hitPoints.length || track.candidateEvents.length)); + const latestMissing = latestChartQuery.error instanceof ApiError + && latestChartQuery.error.code === 'CHART_NOT_GENERATED'; + + if (projectQuery.isLoading) return
正在加载工程…
; + if (projectQuery.isError || !project) return
无法打开工程

{projectQuery.error?.message ?? '工程不存在'}

返回歌曲工作区
; + if (!track) return
工程还没有音轨返回歌曲工作区
; + + return ( +
+
+
+ + ← 声学编辑器 +
AI CHART WORKSPACE

{project.title}

{project.artist || '未知艺术家'} · {track.originalFileName}

+
+ +
+ +
+
SONG{project.title}
+
BPM{track.tempoMap[0]?.bpm.toFixed(2).replace(/\.00$/, '') ?? '—'}
+
DURATION{formatTime(track.durationSec).slice(0, -4)}
+
CANDIDATES{track.candidateEvents.length.toLocaleString()}
+
HIT POINTS{track.hitPoints.length.toLocaleString()}
+
+ +
+ + +
+ {latestChartQuery.isLoading && !chart ?
正在读取最近生成谱面…
: null} + {latestChartQuery.isError && !latestMissing && !chart ?
无法读取谱面

{latestChartQuery.error.message}

: null} + {chart ? ( + <> +
{chart.generator.replaceAll('_', ' ')}

{chart.title}

{chart.modelProvenance ?

Local Transformer · {chart.modelProvenance.architecture ?? 'checkpoint'} · 本地授权数据

:

Reference corpus statistical generator

}
Lv.{chart.meter}
+ + + ) : !latestChartQuery.isLoading || latestMissing ? ( +
准备生成第一张谱面

真实 BeatForge 候选会被分配到五个身体面板,并经过密度与可玩性验证。

+ ) : null} +
+ + +
+
+ ); +} diff --git a/apps/web/src/pages/WorkspacePage.tsx b/apps/web/src/pages/WorkspacePage.tsx index 7f93cb6..e571af6 100644 --- a/apps/web/src/pages/WorkspacePage.tsx +++ b/apps/web/src/pages/WorkspacePage.tsx @@ -1,5 +1,6 @@ import { useMemo, useState } from 'react'; import { useQuery } from '@tanstack/react-query'; +import { Link } from 'react-router-dom'; import { api } from '../api/client'; import { Brand } from '../components/Brand'; import { ProjectCard } from '../components/ProjectCard'; @@ -37,7 +38,10 @@ export function WorkspacePage() {
LIBRARY / 本地工作区

歌曲工作区

- +
+ 真实谱面库 + +
diff --git a/apps/web/src/styles/global.css b/apps/web/src/styles/global.css index ec8ef36..6c2ce12 100644 --- a/apps/web/src/styles/global.css +++ b/apps/web/src/styles/global.css @@ -3051,6 +3051,954 @@ input[type="range"] { width: 92px; } +/* AI Chart Engine */ +.workspace-header-actions, +.chart-engine-header-left, +.chart-engine-header nav, +.chart-generation-header nav, +.project-card-actions { + display: flex; + align-items: center; +} + +.workspace-header-actions { + margin-left: auto; + gap: 8px; +} + +.chart-engine-link, +.chart-workspace-button, +.chart-card-link { + color: var(--accent-strong); +} + +.project-card-actions { + margin-left: auto; + gap: 10px; +} + +.project-card-actions .text-button { + margin-left: 0; +} + +.chart-engine-shell, +.chart-generation-shell { + display: flex; + height: 100dvh; + min-height: 0; + overflow: hidden; + flex-direction: column; + background: var(--bg-primary); +} + +.chart-engine-header, +.chart-generation-header { + display: flex; + min-height: 76px; + padding: 0 18px; + align-items: center; + justify-content: space-between; + gap: 18px; + border-bottom: 1px solid var(--border); + background: var(--bg-panel); +} + +.chart-engine-header-left { + min-width: 0; + gap: 13px; +} + +.chart-engine-header-left > div:last-child { + min-width: 0; +} + +.chart-engine-header h1, +.chart-generation-header h1, +.chart-panel-heading h2, +.reference-chart-header h2, +.generated-chart-heading h2 { + margin: 0; +} + +.chart-engine-header h1, +.chart-generation-header h1 { + margin-top: 3px; + font-size: 17px; +} + +.chart-generation-header p, +.reference-chart-header p, +.chart-panel-heading p { + margin: 4px 0 0; + color: var(--text-secondary); +} + +.chart-generation-header p { + overflow: hidden; + max-width: 460px; + font-size: 9px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.chart-engine-header nav, +.chart-generation-header nav { + min-width: max-content; + gap: 8px; +} + +.real-data-badge { + padding: 7px 9px; + border: 1px solid color-mix(in srgb, var(--success) 38%, var(--border)); + border-radius: var(--radius-sm); + background: var(--success-soft); + color: var(--success); + font-size: 9px; +} + +.chart-corpus-strip, +.chart-song-strip { + display: grid; + min-height: 60px; + flex: 0 0 60px; + border-bottom: 1px solid var(--border); + background: var(--bg-subtle); + grid-template-columns: repeat(5, minmax(0, 1fr)); +} + +.chart-corpus-strip > div, +.chart-song-strip > div { + display: flex; + min-width: 0; + padding: 10px 16px; + justify-content: center; + flex-direction: column; + border-right: 1px solid var(--border); +} + +.chart-corpus-strip > div:last-child, +.chart-song-strip > div:last-child { + border-right: 0; +} + +.chart-corpus-strip small, +.chart-song-strip small, +.reference-chart-facts small, +.validator-metrics small, +.generation-corpus-result small { + color: var(--text-tertiary); + font-size: 7px; + letter-spacing: .08em; +} + +.chart-corpus-strip strong, +.chart-song-strip strong { + margin-top: 4px; + overflow: hidden; + font-size: 12px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.chart-library-layout { + display: grid; + min-height: 0; + flex: 1 1 auto; + grid-template-columns: 336px minmax(0, 1fr); +} + +.reference-library-panel { + display: flex; + min-height: 0; + padding: 16px 0 0; + flex-direction: column; + border-right: 1px solid var(--border); + background: var(--bg-panel); +} + +.chart-panel-heading { + padding: 0 16px 13px; +} + +.chart-panel-heading h2 { + margin-top: 4px; + font-size: 15px; +} + +.chart-panel-heading p { + font-size: 9px; + line-height: 1.5; +} + +.chart-library-search { + display: flex; + height: 34px; + margin: 0 12px; + padding: 0 9px; + align-items: center; + gap: 7px; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: var(--bg-primary); + color: var(--text-tertiary); +} + +.chart-library-search:focus-within { + border-color: var(--border-focus); + box-shadow: var(--focus-ring); +} + +.chart-library-search input { + min-width: 0; + flex: 1; + border: 0; + outline: 0; + background: transparent; + color: var(--text-primary); + font-size: 10px; +} + +.chart-group-tabs { + display: flex; + margin: 9px 12px 0; + gap: 3px; +} + +.chart-group-tabs button { + height: 27px; + padding: 0 8px; + flex: 1; + border-color: transparent; + background: transparent; + color: var(--text-secondary); + font-size: 8px; +} + +.chart-group-tabs button.active { + border-color: var(--border-strong); + background: var(--bg-muted); + color: var(--text-primary); +} + +.reference-chart-count { + padding: 9px 14px 7px; + color: var(--text-tertiary); + font-size: 8px; +} + +.reference-chart-count strong { + color: var(--text-secondary); +} + +.reference-chart-list { + min-height: 0; + padding-bottom: 12px; + overflow-y: auto; + flex: 1; + border-top: 1px solid var(--border); +} + +.reference-chart-item { + display: flex; + width: 100%; + min-height: 76px; + padding: 10px 13px; + align-items: stretch; + flex-direction: column; + gap: 4px; + text-align: left; + border: 0; + border-bottom: 1px solid color-mix(in srgb, var(--border) 70%, transparent); + border-radius: 0; + background: transparent; +} + +.reference-chart-item:hover { + background: var(--bg-hover); +} + +.reference-chart-item.active { + background: var(--accent-soft); + box-shadow: inset 3px 0 var(--accent); +} + +.reference-chart-item > span { + display: flex; + align-items: center; + justify-content: space-between; +} + +.reference-chart-item small { + color: var(--text-tertiary); + font-size: 7px; + letter-spacing: .08em; +} + +.reference-chart-item b { + color: var(--accent-strong); + font-size: 9px; +} + +.reference-chart-item strong { + overflow: hidden; + color: var(--text-primary); + font-size: 10px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.reference-chart-item p { + margin: 0; + color: var(--text-secondary); + font-size: 8px; +} + +.reference-list-state { + padding: 26px 16px; + text-align: center; + color: var(--text-secondary); + font-size: 9px; +} + +.reference-list-state p { + margin: 6px 0; +} + +.reference-preview-workspace { + display: grid; + min-width: 0; + min-height: 0; + padding: 18px; + overflow: hidden; + background: var(--bg-primary); + grid-template-rows: auto auto minmax(0, 1fr); +} + +.reference-chart-header, +.generated-chart-heading { + display: flex; + max-width: 880px; + margin: 0 auto; + padding: 0 0 12px; + align-items: flex-end; + justify-content: space-between; + gap: 16px; +} + +.reference-chart-header h2, +.generated-chart-heading h2 { + margin-top: 4px; + font-size: 18px; +} + +.reference-chart-header p { + font-size: 9px; +} + +.reference-chart-meter { + display: flex; + min-width: 68px; + padding-left: 14px; + align-items: flex-end; + flex-direction: column; + border-left: 1px solid var(--border); +} + +.reference-chart-meter small { + color: var(--text-tertiary); + font-size: 8px; +} + +.reference-chart-meter strong { + margin-top: 3px; + color: var(--accent-strong); + font-size: 20px; +} + +.reference-chart-facts { + display: grid; + max-width: 880px; + margin: 0 auto 12px; + grid-template-columns: repeat(6, minmax(0, 1fr)); + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: var(--bg-panel); +} + +.reference-chart-facts > span { + display: flex; + min-width: 0; + padding: 9px 10px; + flex-direction: column; + border-right: 1px solid var(--border); +} + +.reference-chart-facts > span:last-child { + border-right: 0; +} + +.reference-chart-facts strong { + margin-top: 3px; + overflow: hidden; + font-size: 10px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.chart-full-state { + display: grid; + min-height: 360px; + padding: 28px; + place-content: center; + gap: 9px; + text-align: center; + color: var(--text-secondary); +} + +.chart-full-state p { + max-width: 460px; + margin: 0; + font-size: 10px; + line-height: 1.5; +} + +.chart-player { + display: flex; + width: 100%; + height: 100%; + min-height: 0; + max-width: 880px; + margin: 0 auto; + overflow: hidden; + flex-direction: column; + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: var(--bg-panel); + box-shadow: var(--shadow-soft); + container-type: inline-size; +} + +.five-lane-preview { + width: 100%; + min-height: 0; + overflow: hidden; + flex: 1 1 auto; + background: var(--chart-canvas-bg); +} + +.five-lane-preview canvas { + display: block; + max-width: 100%; +} + +.chart-audio-error { + margin: 8px; +} + +.chart-transport { + display: flex; + min-height: 62px; + padding: 9px 12px; + align-items: center; + gap: 9px; + border-top: 1px solid var(--border); + background: var(--bg-panel); +} + +.chart-transport .transport-secondary { + flex: 0 0 34px; +} + +.chart-transport .play-button { + flex: 0 0 40px; +} + +.chart-transport-time { + display: flex; + min-width: 124px; + align-items: baseline; + gap: 5px; + font-variant-numeric: tabular-nums; +} + +.chart-transport-time strong { + font-size: 13px; +} + +.chart-transport-time span { + color: var(--text-tertiary); + font-size: 9px; +} + +.chart-scroll-speed-control { + display: flex; + height: 34px; + padding-left: 8px; + align-items: center; + flex: 0 0 auto; + gap: 6px; + border-left: 1px solid var(--border); + color: var(--text-tertiary); + font-size: 8px; +} + +.chart-scroll-speed-control select { + height: 30px; + min-width: 62px; + padding: 0 7px; + border: 1px solid var(--border-strong); + border-radius: var(--radius-sm); + outline: 0; + background: var(--bg-primary); + color: var(--text-primary); + font-size: 10px; + font-weight: 650; +} + +.chart-scroll-speed-control select:focus { + border-color: var(--border-focus); + box-shadow: var(--focus-ring); +} + +.chart-seek-control { + display: flex; + min-width: 120px; + flex: 1; +} + +.chart-seek-control input { + width: 100%; + accent-color: var(--accent); +} + +@container (max-width: 540px) { + .chart-transport { + flex-wrap: wrap; + gap: 7px 6px; + } + + .chart-transport-time { + min-width: 0; + flex: 1 1 80px; + } + + .chart-seek-control { + min-width: 100%; + order: 2; + flex-basis: 100%; + } +} + +@container (max-width: 380px) { + .chart-scroll-speed-control > span, + .chart-transport-time > span { + display: none; + } +} + +.chart-generation-header { + flex: 0 0 76px; +} + +.chart-generation-layout { + display: grid; + min-height: 0; + flex: 1 1 auto; + grid-template-columns: 282px minmax(390px, 1fr) 300px; +} + +.chart-generation-controls, +.chart-validation-panel { + min-height: 0; + padding-top: 17px; + overflow-y: auto; + background: var(--bg-panel); +} + +.chart-generation-controls { + border-right: 1px solid var(--border); +} + +.chart-validation-panel { + border-left: 1px solid var(--border); +} + +.validator-footwork { + display: flex; + margin: 10px 16px 0; + padding: 10px 11px; + align-items: center; + justify-content: space-between; + gap: 10px; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: var(--bg-primary); +} + +.validator-footwork > span { + display: flex; + min-width: 0; + flex-direction: column; + gap: 3px; +} + +.validator-footwork small { + color: var(--text-tertiary); + font-size: 7px; + letter-spacing: .08em; +} + +.validator-footwork b, +.validator-footwork strong { + font-size: 9px; +} + +.validator-footwork.valid { + border-color: color-mix(in srgb, var(--accent) 50%, var(--border)); +} + +.validator-footwork.valid strong { + color: var(--accent); +} + +.validator-footwork.invalid { + border-color: color-mix(in srgb, var(--danger) 55%, var(--border)); +} + +.validator-footwork.invalid strong { + color: var(--danger); +} + +.difficulty-control { + display: flex; + padding: 15px 16px; + flex-direction: column; + gap: 9px; + border-top: 1px solid var(--border); + border-bottom: 1px solid var(--border); +} + +.difficulty-control > span { + display: flex; + align-items: center; + justify-content: space-between; +} + +.difficulty-control strong, +.spin-control strong { + font-size: 10px; +} + +.difficulty-control > span small { + color: var(--text-tertiary); + font-size: 7px; + letter-spacing: .08em; +} + +.difficulty-control select { + width: 100%; + height: 36px; + padding: 0 10px; + border: 1px solid var(--border-strong); + border-radius: var(--radius-sm); + outline: 0; + background: var(--bg-primary); + color: var(--text-primary); + font-size: 11px; + font-weight: 650; +} + +.difficulty-control select:focus { + border-color: var(--border-focus); + box-shadow: var(--focus-ring); +} + +.spin-control { + display: grid; + padding: 15px 16px; + align-items: center; + gap: 9px; + grid-template-columns: minmax(0, 1fr) auto 28px; + border-bottom: 1px solid var(--border); +} + +.spin-control > span { + display: flex; + flex-direction: column; + gap: 3px; +} + +.spin-control small { + color: var(--text-tertiary); + font-size: 8px; +} + +.spin-control b { + color: var(--text-secondary); + font-size: 8px; +} + +.generation-source-note, +.generation-requirement, +.generation-corpus-result { + margin: 13px 15px 0; + padding: 10px; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: var(--bg-subtle); + color: var(--text-secondary); + font-size: 8px; + line-height: 1.5; +} + +.generation-source-note i { + display: inline-block; + width: 6px; + height: 6px; + margin-right: 5px; + border-radius: 50%; + background: var(--success); +} + +.generation-requirement { + border-color: color-mix(in srgb, var(--warning) 35%, var(--border)); + background: var(--warning-soft); + color: var(--warning); +} + +.chart-generation-controls > .error-banner { + margin: 13px 15px 0; + font-size: 9px; +} + +.generate-chart-button { + display: flex; + width: calc(100% - 30px); + min-height: 39px; + margin: 14px 15px 0; + align-items: center; + justify-content: center; + gap: 7px; +} + +.generation-corpus-result { + display: flex; + flex-direction: column; + gap: 3px; +} + +.generation-corpus-result strong { + color: var(--text-primary); + font-size: 9px; +} + +.generation-model-result { + margin-top: 4px; + padding-top: 5px; + border-top: 1px solid var(--border); + color: var(--accent-strong); +} + +.generated-chart-preview { + display: grid; + min-width: 0; + min-height: 0; + padding: 17px; + overflow: hidden; + background: var(--bg-primary); + grid-template-rows: auto minmax(0, 1fr); +} + +.generated-chart-heading { + max-width: 760px; +} + +.generated-chart-heading > strong { + color: var(--accent-strong); + font-size: 18px; +} + +.generated-chart-heading p { + margin: 4px 0 0; + color: var(--text-tertiary); + font-size: 8px; +} + +.generated-chart-preview .chart-player { + max-width: 760px; +} + +.reference-preview-workspace > .chart-full-state, +.generated-chart-preview > .chart-full-state { + min-height: 0; + grid-row: 1 / -1; +} + +.chart-empty-preview { + height: 100%; + min-height: 430px; +} + +.empty-preview-lanes { + display: grid; + width: 220px; + height: 128px; + margin: 0 auto 12px; + grid-template-columns: repeat(5, 1fr); + border: 1px solid var(--border); + background: var(--chart-canvas-bg); +} + +.empty-preview-lanes i { + border-right: 1px solid var(--chart-lane-divider); +} + +.empty-preview-lanes i:last-child { + border-right: 0; +} + +.validator-score { + display: flex; + margin: 0 15px; + padding: 13px; + align-items: center; + gap: 11px; + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: var(--bg-subtle); +} + +.validator-score > strong { + font-size: 30px; + font-variant-numeric: tabular-nums; +} + +.validator-score.valid > strong, +.validator-score.valid b { + color: var(--success); +} + +.validator-score.invalid > strong, +.validator-score.invalid b { + color: var(--danger); +} + +.validator-score > span { + display: flex; + flex-direction: column; + gap: 3px; +} + +.validator-score b { + font-size: 10px; +} + +.validator-score small { + color: var(--text-tertiary); + font-size: 8px; +} + +.validator-metrics { + display: grid; + margin: 12px 15px 0; + grid-template-columns: 1fr 1fr; + border: 1px solid var(--border); + border-radius: var(--radius-sm); +} + +.validator-metrics > span { + display: flex; + padding: 9px; + flex-direction: column; + border-right: 1px solid var(--border); + border-bottom: 1px solid var(--border); +} + +.validator-metrics > span:nth-child(2n) { + border-right: 0; +} + +.validator-metrics > span:nth-last-child(-n + 2) { + border-bottom: 0; +} + +.validator-metrics strong { + margin-top: 3px; + font-size: 11px; +} + +.validator-issues { + display: flex; + margin: 12px 15px 16px; + padding: 0; + flex-direction: column; + gap: 7px; + list-style: none; +} + +.validator-issues li { + padding: 9px; + border: 1px solid var(--border); + border-left: 3px solid var(--warning); + border-radius: var(--radius-sm); + background: var(--bg-subtle); +} + +.validator-issues li.severity-error { + border-left-color: var(--danger); +} + +.validator-issues li.severity-info { + border-left-color: var(--accent); +} + +.validator-issues li > span { + color: var(--text-tertiary); + font-size: 7px; + text-transform: uppercase; +} + +.validator-issues li strong { + display: block; + margin-top: 3px; + font-size: 8px; +} + +.validator-issues li p { + margin: 4px 0; + color: var(--text-secondary); + font-size: 8px; + line-height: 1.4; +} + +.validator-issues li small { + color: var(--text-tertiary); + font-size: 7px; +} + +.validator-clean, +.validator-pending { + margin: 12px 15px; + padding: 13px; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-secondary); + font-size: 9px; + line-height: 1.5; +} + +.validator-clean { + border-color: color-mix(in srgb, var(--success) 35%, var(--border)); + background: var(--success-soft); + color: var(--success); +} + +.chart-sm-export { + min-height: 32px; + padding: 0 13px; + font-size: 10px; +} + /* Responsive */ @media (max-width: 1180px) { .editor-topline { @@ -3075,6 +4023,10 @@ input[type="range"] { .alignment-workspace { grid-template-columns: minmax(0, 1fr) 290px; } + + .chart-generation-layout { + grid-template-columns: 250px minmax(360px, 1fr) 270px; + } } @media (max-width: 900px) { @@ -3138,6 +4090,36 @@ input[type="range"] { .editor-guide-steps { grid-template-columns: 1fr 1fr; } + + .chart-engine-shell, + .chart-generation-shell { + height: auto; + min-height: 100vh; + overflow: visible; + } + + .chart-library-layout, + .chart-generation-layout { + display: block; + } + + .reference-library-panel, + .chart-generation-controls, + .chart-validation-panel { + max-height: none; + overflow: visible; + border-right: 0; + border-left: 0; + border-bottom: 1px solid var(--border); + } + + .reference-chart-list { + max-height: 360px; + } + + .chart-validation-panel { + padding-bottom: 14px; + } } @media (max-width: 640px) { @@ -3192,6 +4174,50 @@ input[type="range"] { .volume-control { display: none; } + + .chart-engine-header, + .chart-generation-header { + min-height: 68px; + padding: 8px 11px; + } + + .chart-engine-header .brand-copy, + .chart-generation-header .brand-copy, + .chart-engine-header .header-divider, + .chart-generation-header .back-link { + display: none; + } + + .real-data-badge, + .chart-engine-header nav .toolbar-button, + .chart-generation-header nav .toolbar-button { + display: none; + } + + .chart-corpus-strip, + .chart-song-strip { + overflow-x: auto; + grid-template-columns: repeat(5, minmax(118px, 1fr)); + } + + .reference-preview-workspace, + .generated-chart-preview { + padding: 12px; + } + + .reference-chart-facts { + overflow-x: auto; + grid-template-columns: repeat(6, minmax(100px, 1fr)); + } + + .chart-transport { + flex-wrap: wrap; + } + + .chart-seek-control { + min-width: 100%; + order: 2; + } } @media (prefers-reduced-motion: reduce) { diff --git a/apps/web/src/styles/tokens.css b/apps/web/src/styles/tokens.css index 842bbae..3c42c7b 100644 --- a/apps/web/src/styles/tokens.css +++ b/apps/web/src/styles/tokens.css @@ -72,4 +72,17 @@ --canvas-minimap-waveform: #4FA89A73; --canvas-minimap-viewport: #A3ADAE14; --canvas-minimap-viewport-border: #A3ADAE52; + + --chart-canvas-bg: #070C0E; + --chart-lane: #0A1114; + --chart-lane-alternate: #0D1518; + --chart-lane-divider: #253138; + --chart-receptor: #A7C8C1; + --chart-judgment: #D26B76; + --chart-hold: #4FA89A99; + --chart-note-left-down: #628FBA; + --chart-note-left-up: #9180A9; + --chart-note-center: #D1B75F; + --chart-note-right-up: #BF7188; + --chart-note-right-down: #C18753; } diff --git a/apps/web/src/types/index.ts b/apps/web/src/types/index.ts index 82f97de..707bd84 100644 --- a/apps/web/src/types/index.ts +++ b/apps/web/src/types/index.ts @@ -405,3 +405,179 @@ export interface HitDisplayFilters { } export type GridSubdivision = '1/4' | '1/8' | '1/12' | '1/16' | '1/24' | '1/32'; + +export type ChartMode = 'pump-single' | 'pump-double'; +export type ChartNoteType = 'tap' | 'hold' | 'mine'; +export type ChartIssueSeverity = 'info' | 'warning' | 'error'; +export type ReferenceChartGroup = 'SPEED_CLUB' | 'SPEED_DEVIL' | 'SPEED_REMIX'; + +export interface ChartTempoPoint { + beat: number; + bpm: number; + timeSec: number; +} + +export interface ChartNote { + lane: number; + type: ChartNoteType; + endTimeSec: number | null; + endBeat: number | null; + source: string; + confidence: number; + foot: 'left' | 'right' | null; +} + +export interface ChartEvent { + timeSec: number; + beat: number; + measure: number; + subdivision: number; + rowIndex: number | null; + notes: ChartNote[]; + sourceEventId: string | null; + sourceEventIds?: string[]; + sourceHitPointIds?: string[]; + anchorPriority?: 0 | 1 | 2; + pattern: string | null; +} + +export interface ChartStatistics { + noteCount: number; + eventCount: number; + holdCount: number; + jumpCount: number; + mineCount: number; + durationSec: number; + npsAverage: number; + npsPeak: number; + singleRatio: number; + jumpRatio: number; + holdRatio: number; + laneCounts: number[]; + measureDensities: number[]; + sameFootRuns: number[]; + footSwitchRatio: number; + smallSpinCount: number; + bigSpinCount: number; +} + +export interface ChartValidationIssue { + code: string; + severity: ChartIssueSeverity; + message: string; + timeSec: number | null; + beat: number | null; + penalty: number; +} + +export interface ChartValidationResult { + valid: boolean; + score: number; + issues: ChartValidationIssue[]; + metrics: Record; +} + +export interface ChartModelProvenance { + schemaVersion: string | number | null; + architecture: string | null; + createdAt: string | null; + datasetFingerprint: string | null; + sampleCount: number | null; + bestLoss: number | null; + realDataOnly: boolean; + checkpointSha256: string | null; +} + +export interface ChartDocument { + id: string; + title: string; + artist: string; + music: string; + sourceGroup: string | null; + sourcePath: string | null; + mode: ChartMode; + laneCount: number; + difficulty: string; + meter: number; + bpm: number; + offsetSec: number; + durationSec: number; + measureCount: number; + tempoMap: ChartTempoPoint[]; + events: ChartEvent[]; + statistics: ChartStatistics | null; + validation: ChartValidationResult | null; + optimization: Record | null; + modelProvenance: ChartModelProvenance | null; + generator: string; + generatorVersion: string; + seed: number | null; + spinEnabled?: boolean; +} + +export interface ReferenceChartSummary { + id: string; + title: string; + group: ReferenceChartGroup; + mode: ChartMode; + laneCount: number; + difficulty: string; + meter: number; + bpm: number; + bpmMax: number; + offsetSec: number; + durationSec: number; + noteCount: number; + eventCount: number; + npsAverage: number; + npsPeak: number; + audioUrl: string; + chartUrl: string; +} + +export interface ReferenceChartListResponse { + items: ReferenceChartSummary[]; + total: number; + corpusTotal: number; + source: 'local_reference_corpus' | string; +} + +export interface ChartCorpusStatistics { + chartCount: number; + songCount: number; + singleChartCount: number; + singleSongCount: number; + doubleChartCount: number; + doubleSongCount: number; + difficultyMin: number; + difficultyMax: number; + totalNotes: number; + totalDurationSec: number; + averageNps: number; + groups: Record; + laneTransitionProbabilities: number[][]; + meterProfiles: Record>; +} + +export interface GenerateChartRequest { + difficulty: number; + enableSpin: boolean; + useLocalModel?: boolean; + seed?: number; +} + +export interface ChartGenerationResponse { + generationId: string; + chart: ChartDocument; + referenceCorpus: { + source: string; + chartCount: number; + songCount: number; + difficultyRange: number[]; + model: { + requested: boolean; + available: boolean; + used: boolean; + }; + }; +} diff --git a/apps/web/src/utils/chartPreview.ts b/apps/web/src/utils/chartPreview.ts new file mode 100644 index 0000000..e63f8ba --- /dev/null +++ b/apps/web/src/utils/chartPreview.ts @@ -0,0 +1,97 @@ +import type { ChartEvent, ChartNoteType } from '../types'; + +export const FIVE_PANEL_LANES = [0, 1, 2, 3, 4] as const; +export type FivePanelLane = (typeof FIVE_PANEL_LANES)[number]; + +export const CHART_SCROLL_SPEED_OPTIONS = [1, 2, 3, 4, 5, 6, 8] as const; +export type ChartScrollSpeed = (typeof CHART_SCROLL_SPEED_OPTIONS)[number]; +export const DEFAULT_CHART_SCROLL_SPEED: ChartScrollSpeed = 4; +export const BASE_CHART_APPROACH_SECONDS = 2.6; + +export const FIVE_PANEL_LABELS: Record = { + 0: '左下', + 1: '左上', + 2: '中心', + 3: '右上', + 4: '右下', +}; + +export function isChartScrollSpeed(value: number): value is ChartScrollSpeed { + return CHART_SCROLL_SPEED_OPTIONS.includes(value as ChartScrollSpeed); +} + +export function chartApproachSeconds( + scrollSpeed: number, + baseApproachSeconds = BASE_CHART_APPROACH_SECONDS, +): number { + const safeBase = Number.isFinite(baseApproachSeconds) && baseApproachSeconds > 0 + ? baseApproachSeconds + : BASE_CHART_APPROACH_SECONDS; + if (!Number.isFinite(scrollSpeed) || scrollSpeed <= 0) return safeBase; + return safeBase / scrollSpeed; +} + +export interface RenderableChartNote { + key: string; + lane: FivePanelLane; + type: ChartNoteType; + startTimeSec: number; + endTimeSec: number | null; + beat: number; + measure: number; + pattern: string | null; +} + +export function flattenFivePanelEvents(events: readonly ChartEvent[]): RenderableChartNote[] { + const notes: RenderableChartNote[] = []; + events.forEach((event, eventIndex) => { + event.notes.forEach((note, noteIndex) => { + if (!FIVE_PANEL_LANES.includes(note.lane as FivePanelLane)) return; + notes.push({ + key: `${event.sourceEventId ?? eventIndex}:${noteIndex}:${note.lane}`, + lane: note.lane as FivePanelLane, + type: note.type, + startTimeSec: event.timeSec, + endTimeSec: note.endTimeSec, + beat: event.beat, + measure: event.measure, + pattern: event.pattern, + }); + }); + }); + return notes.sort((left, right) => ( + left.startTimeSec - right.startTimeSec || left.lane - right.lane + )); +} + +export function visibleChartNotes( + notes: readonly RenderableChartNote[], + currentTimeSec: number, + approachSeconds: number, + missWindowSeconds = 0.35, +): RenderableChartNote[] { + const latestStart = currentTimeSec + Math.max(0, approachSeconds); + const earliestVisibleEnd = currentTimeSec - Math.max(0, missWindowSeconds); + return notes.filter((note) => ( + note.startTimeSec <= latestStart + && (note.endTimeSec ?? note.startTimeSec) >= earliestVisibleEnd + )); +} + +export function chartNoteY( + eventTimeSec: number, + currentTimeSec: number, + judgmentLineY: number, + travelDistance: number, + approachSeconds: number, +): number { + if (!Number.isFinite(approachSeconds) || approachSeconds <= 0) return judgmentLineY; + return judgmentLineY + + ((eventTimeSec - currentTimeSec) / approachSeconds) * Math.max(0, travelDistance); +} + +export function clampPlaybackTime(timeSec: number, durationSec: number): number { + if (!Number.isFinite(timeSec)) return 0; + const safeDuration = Number.isFinite(durationSec) ? Math.max(0, durationSec) : 0; + return Math.max(0, Math.min(safeDuration, timeSec)); +} diff --git a/apps/web/src/utils/generationSeed.ts b/apps/web/src/utils/generationSeed.ts new file mode 100644 index 0000000..34b9d0a --- /dev/null +++ b/apps/web/src/utils/generationSeed.ts @@ -0,0 +1,23 @@ +const GENERATION_SEED_RANGE = 0x1_0000_0000; + +function initialGenerationSeed(): number { + if (typeof globalThis.crypto?.getRandomValues === 'function') { + const value = new Uint32Array(1); + globalThis.crypto.getRandomValues(value); + return value[0] ?? 0; + } + return Date.now() % GENERATION_SEED_RANGE; +} + +/** + * Returns a stable per-workspace sequence so every regeneration gets a new, + * explicit seed while requests with an explicitly reused seed remain reproducible. + */ +export function createGenerationSeedSequence(start = initialGenerationSeed()): () => number { + let nextSeed = Math.trunc(start) >>> 0; + return () => { + const seed = nextSeed; + nextSeed = (nextSeed + 1) % GENERATION_SEED_RANGE; + return seed; + }; +} diff --git a/apps/web/tests/chartGenerationPage.test.tsx b/apps/web/tests/chartGenerationPage.test.tsx new file mode 100644 index 0000000..e17199e --- /dev/null +++ b/apps/web/tests/chartGenerationPage.test.tsx @@ -0,0 +1,122 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { api } from '../src/api/client'; +import { ChartGenerationPage } from '../src/pages/ChartGenerationPage'; +import type { ChartGenerationResponse, GenerateChartRequest, ProjectDetail } from '../src/types'; +import { hit, sampleRate, tempo } from './fixtures'; +import { referenceExcerptChart } from './syntheticChartFixtures'; + +const trackId = 'track-generation'; + +const project: ProjectDetail = { + id: 'project-generation', + title: 'Regeneration Test', + artist: 'BeatForge', + genre: 'SPEED', + coverUrl: '', + status: 'completed', + createdAt: '2026-07-21T00:00:00.000Z', + updatedAt: '2026-07-21T00:00:00.000Z', + trackId, + track: { + id: trackId, + projectId: 'project-generation', + createdAt: '2026-07-21T00:00:00.000Z', + updatedAt: '2026-07-21T00:00:00.000Z', + originalFileName: 'regeneration.mp3', + audioUrl: `/api/tracks/${trackId}/audio`, + format: 'mp3', + originalSampleRate: sampleRate, + channels: 2, + sampleCount: Math.round(referenceExcerptChart.durationSec * sampleRate), + durationSec: referenceExcerptChart.durationSec, + leadingSilenceSamples: 0, + analysis: null, + tempoMap: [tempo], + hitPoints: [hit()], + candidateEvents: [], + stems: [{ source: 'mix', available: true, waveformUrl: '/waveform' }], + focusMap: [], + waveformUrl: '/waveform', + }, +}; + +function response(index: number, seed: number): ChartGenerationResponse { + const id = `generated-chart-${index}`; + return { + generationId: id, + chart: { ...referenceExcerptChart, id, title: project.title, seed }, + referenceCorpus: { + source: 'local reference corpus', + chartCount: 12, + songCount: 9, + difficultyRange: [1, 15], + model: { requested: true, available: true, used: true }, + }, + }; +} + +function renderPage() { + const client = new QueryClient({ + defaultOptions: { queries: { retry: false }, mutations: { retry: false } }, + }); + return render( + + + + } /> + + + , + ); +} + +afterEach(() => vi.restoreAllMocks()); + +describe('ChartGenerationPage regeneration', () => { + it('uses a fresh explicit seed, displays the new generation, and resets playback each time', async () => { + const user = userEvent.setup(); + const requests: GenerateChartRequest[] = []; + vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(null); + vi.spyOn(api, 'getProject').mockResolvedValue(project); + vi.spyOn(api, 'getLatestChart').mockResolvedValue({ + ...referenceExcerptChart, + id: 'latest-chart', + title: project.title, + }); + vi.spyOn(api, 'generateChart').mockImplementation(async (_requestedTrackId, input) => { + requests.push(input); + return response(requests.length, input.seed!); + }); + + renderPage(); + + const regenerate = await screen.findByRole('button', { name: '重新生成谱面' }); + const difficultySelect = screen.getByRole('combobox', { name: '谱面难度' }); + expect(difficultySelect).toHaveValue(String(referenceExcerptChart.meter)); + expect(difficultySelect.querySelectorAll('option')).toHaveLength(15); + await user.selectOptions(difficultySelect, '11'); + await user.click(regenerate); + await waitFor(() => expect(screen.getByRole('link', { name: '导出 SM' })) + .toHaveAttribute('href', expect.stringContaining('generationId=generated-chart-1'))); + + const firstPosition = screen.getByRole('slider', { name: '播放位置' }); + fireEvent.change(firstPosition, { target: { value: '12' } }); + expect((firstPosition as HTMLInputElement).value).toBe('12'); + + await user.click(screen.getByRole('button', { name: '重新生成谱面' })); + await waitFor(() => expect(screen.getByRole('link', { name: '导出 SM' })) + .toHaveAttribute('href', expect.stringContaining('generationId=generated-chart-2'))); + + expect(requests).toHaveLength(2); + expect(requests[0]).toMatchObject({ difficulty: 11, enableSpin: false, useLocalModel: true }); + expect(requests[0]?.seed).toEqual(expect.any(Number)); + expect(requests[1]?.seed).toEqual(expect.any(Number)); + expect(requests[1]?.seed).not.toBe(requests[0]?.seed); + expect((screen.getByRole('slider', { name: '播放位置' }) as HTMLInputElement).value).toBe('0'); + expect(screen.getByTestId('five-lane-preview')).toHaveAttribute('data-current-time', '0.000'); + }); +}); diff --git a/apps/web/tests/chartPlayer.test.tsx b/apps/web/tests/chartPlayer.test.tsx new file mode 100644 index 0000000..e5abe0a --- /dev/null +++ b/apps/web/tests/chartPlayer.test.tsx @@ -0,0 +1,33 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { ChartPlayer } from '../src/components/ChartPlayer'; +import { referenceExcerptChart } from './syntheticChartFixtures'; + +afterEach(() => vi.restoreAllMocks()); + +describe('ChartPlayer scroll speed', () => { + it('defaults to 4x visual speed and never changes the audio playback rate', () => { + vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(null); + const { container } = render( + , + ); + + const audio = container.querySelector('audio'); + expect(audio).not.toBeNull(); + expect(audio?.playbackRate).toBe(1); + expect(screen.getByRole('combobox', { name: '谱面流速' })).toHaveValue('4'); + expect(screen.getByTestId('five-lane-preview')).toHaveAttribute('data-scroll-speed', '4'); + + fireEvent.change(screen.getByRole('combobox', { name: '谱面流速' }), { + target: { value: '8' }, + }); + + expect(audio?.playbackRate).toBe(1); + expect(screen.getByTestId('five-lane-preview')).toHaveAttribute('data-scroll-speed', '8'); + expect(screen.getByTestId('five-lane-preview')).toHaveAttribute('data-approach-seconds', '0.325'); + }); +}); diff --git a/apps/web/tests/chartPreview.test.ts b/apps/web/tests/chartPreview.test.ts new file mode 100644 index 0000000..1623885 --- /dev/null +++ b/apps/web/tests/chartPreview.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from 'vitest'; +import { + chartApproachSeconds, + chartNoteY, + clampPlaybackTime, + flattenFivePanelEvents, + visibleChartNotes, +} from '../src/utils/chartPreview'; +import { referenceExcerptEvents } from './syntheticChartFixtures'; + +describe('five-lane chart preview math with synthetic events', () => { + it('maps visual scroll speed to a shorter approach window without changing time', () => { + expect(chartApproachSeconds(1)).toBe(2.6); + expect(chartApproachSeconds(4)).toBe(0.65); + expect(chartApproachSeconds(8)).toBe(0.325); + expect(chartApproachSeconds(0)).toBe(2.6); + }); + + it('flattens taps, a hold, and a simultaneous jump without losing panel lanes', () => { + const notes = flattenFivePanelEvents(referenceExcerptEvents); + expect(notes).toHaveLength(4); + expect(notes.map((note) => note.lane)).toEqual([0, 4, 0, 2]); + expect(notes.find((note) => note.type === 'hold')).toMatchObject({ + startTimeSec: 2, + endTimeSec: 2.5, + lane: 4, + }); + }); + + it('places an event on the judgment line at its absolute parser time and future notes below it', () => { + expect(chartNoteY(2, 2, 112, 444, 2.6)).toBe(112); + expect(chartNoteY(2, 1.5, 112, 444, 2.6)).toBeGreaterThan(112); + expect(chartNoteY(2, 2.25, 112, 444, 2.6)).toBeLessThan(112); + }); + + it('keeps a hold visible after its head crosses the judgment line until its tail passes', () => { + const notes = flattenFivePanelEvents(referenceExcerptEvents); + expect(visibleChartNotes(notes, 2.25, 2.6).some((note) => note.type === 'hold')).toBe(true); + expect(visibleChartNotes(notes, 2.9, 2.6).some((note) => note.type === 'hold')).toBe(false); + }); + + it('clamps rewind and fast-forward to the configured audio duration', () => { + expect(clampPlaybackTime(-5, 20)).toBe(0); + expect(clampPlaybackTime(30, 20)).toBe(20); + expect(clampPlaybackTime(6, 20)).toBe(6); + }); +}); diff --git a/apps/web/tests/chartTransport.test.tsx b/apps/web/tests/chartTransport.test.tsx new file mode 100644 index 0000000..6abafc7 --- /dev/null +++ b/apps/web/tests/chartTransport.test.tsx @@ -0,0 +1,39 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { ChartTransport } from '../src/components/ChartTransport'; + +describe('ChartTransport', () => { + it('exposes play, pause-sized skips, and sample-accurate slider seeking', () => { + const onTogglePlay = vi.fn(); + const onSeek = vi.fn(); + const onSeekBy = vi.fn(); + const onScrollSpeedChange = vi.fn(); + render( + , + ); + + fireEvent.click(screen.getByRole('button', { name: '播放谱面预览' })); + fireEvent.click(screen.getByRole('button', { name: '快退 5 秒' })); + fireEvent.click(screen.getByRole('button', { name: '快进 5 秒' })); + fireEvent.change(screen.getByRole('slider', { name: '播放位置' }), { target: { value: '42.125' } }); + fireEvent.change(screen.getByRole('combobox', { name: '谱面流速' }), { target: { value: '6' } }); + + expect(onTogglePlay).toHaveBeenCalledOnce(); + expect(onSeekBy).toHaveBeenNthCalledWith(1, -5); + expect(onSeekBy).toHaveBeenNthCalledWith(2, 5); + expect(onSeek).toHaveBeenCalledWith(42.125); + expect(onScrollSpeedChange).toHaveBeenCalledWith(6); + expect(screen.getByRole('combobox', { name: '谱面流速' })).toHaveValue('4'); + expect(screen.getByText('0:18.250')).toBeVisible(); + expect(screen.getByText('/ 2:04.500')).toBeVisible(); + }); +}); diff --git a/apps/web/tests/chartValidationPanel.test.tsx b/apps/web/tests/chartValidationPanel.test.tsx new file mode 100644 index 0000000..1ac2cc4 --- /dev/null +++ b/apps/web/tests/chartValidationPanel.test.tsx @@ -0,0 +1,26 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import { ChartValidationPanel } from '../src/components/ChartValidationPanel'; +import { referenceExcerptChart } from './syntheticChartFixtures'; + +describe('ChartValidationPanel full-step result', () => { + it('shows the strict alternating no-spin guarantee returned by the validator', () => { + render( + , + ); + + expect(screen.getByText('FULL STEP · 全步伐')).toBeInTheDocument(); + expect(screen.getByText('严格左右交替 · 无转圈')).toBeInTheDocument(); + expect(screen.getByText('通过')).toBeInTheDocument(); + }); +}); diff --git a/apps/web/tests/editorToolbar.test.tsx b/apps/web/tests/editorToolbar.test.tsx index 47e47ba..436d5c4 100644 --- a/apps/web/tests/editorToolbar.test.tsx +++ b/apps/web/tests/editorToolbar.test.tsx @@ -158,7 +158,11 @@ describe('EditorToolbar', () => { 'href', `/api/tracks/${inputTrack.id}/export?format=package&audio=reference`, ); - expect(within(actions).getAllByRole('link')).toHaveLength(1); + expect(within(actions).getByRole('link', { name: 'AI Chart' })).toHaveAttribute( + 'href', + `/projects/${project(inputTrack).id}/chart`, + ); + expect(within(actions).getAllByRole('link')).toHaveLength(2); expect(within(actions).queryByText('仅数据包')).not.toBeInTheDocument(); expect(within(actions).queryByText('完整分轨包')).not.toBeInTheDocument(); expect(within(actions).queryByText('导出 JSON')).not.toBeInTheDocument(); diff --git a/apps/web/tests/fiveLaneChartPreview.test.tsx b/apps/web/tests/fiveLaneChartPreview.test.tsx new file mode 100644 index 0000000..7e135b8 --- /dev/null +++ b/apps/web/tests/fiveLaneChartPreview.test.tsx @@ -0,0 +1,68 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { FiveLaneChartPreview } from '../src/components/FiveLaneChartPreview'; +import { referenceExcerptChart } from './syntheticChartFixtures'; + +afterEach(() => vi.restoreAllMocks()); + +describe('FiveLaneChartPreview', () => { + it('renders five labeled gameplay lanes and the fixture hold on a Canvas', async () => { + const context = new Proxy( + { + fillText: vi.fn(), + fillRect: vi.fn(), + lineTo: vi.fn(), + moveTo: vi.fn(), + arc: vi.fn(), + } as Record, + { + get(target, property) { + if (!(property in target)) target[property] = vi.fn(); + return target[property]; + }, + }, + ) as unknown as CanvasRenderingContext2D; + vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(context); + + render(); + + expect(screen.getByTestId('five-lane-preview')).toHaveAttribute('data-current-time', '2.500'); + expect(screen.getByTestId('five-lane-preview')).toHaveAttribute('data-scroll-speed', '4'); + expect(screen.getByTestId('five-lane-preview')).toHaveAttribute('data-approach-seconds', '0.650'); + expect(screen.getByLabelText(/五轨谱面预览:Synthetic reference chart/)).toBeVisible(); + await waitFor(() => { + for (const label of ['左下', '左上', '中心', '右上', '右下']) { + expect(context.fillText).toHaveBeenCalledWith(label, expect.any(Number), 28); + } + }); + expect(context.fillRect).toHaveBeenCalledWith( + expect.any(Number), + expect.any(Number), + expect.any(Number), + expect.any(Number), + ); + }); + + it('matches a layout-constrained preview height instead of forcing the legacy fixed height', async () => { + vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(null); + vi.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockImplementation(function getWidth( + this: HTMLElement, + ) { + return this instanceof HTMLCanvasElement ? 300 : 720; + }); + vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockImplementation(function getHeight( + this: HTMLElement, + ) { + return this instanceof HTMLCanvasElement ? 150 : 420; + }); + + render(); + + await waitFor(() => { + expect(screen.getByTestId('five-lane-chart-canvas')).toHaveStyle({ + width: '720px', + height: '420px', + }); + }); + }); +}); diff --git a/apps/web/tests/generationSeed.test.ts b/apps/web/tests/generationSeed.test.ts new file mode 100644 index 0000000..94878ca --- /dev/null +++ b/apps/web/tests/generationSeed.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from 'vitest'; +import { createGenerationSeedSequence } from '../src/utils/generationSeed'; + +describe('createGenerationSeedSequence', () => { + it('returns a different explicit seed for every generation in a workspace', () => { + const takeSeed = createGenerationSeedSequence(41); + + expect([takeSeed(), takeSeed(), takeSeed()]).toEqual([41, 42, 43]); + }); + + it('keeps seeds in the unsigned 32-bit range when the sequence wraps', () => { + const takeSeed = createGenerationSeedSequence(0xffff_ffff); + + expect(takeSeed()).toBe(0xffff_ffff); + expect(takeSeed()).toBe(0); + }); +}); diff --git a/apps/web/tests/syntheticChartFixtures.ts b/apps/web/tests/syntheticChartFixtures.ts new file mode 100644 index 0000000..2197c65 --- /dev/null +++ b/apps/web/tests/syntheticChartFixtures.ts @@ -0,0 +1,80 @@ +import type { ChartDocument, ChartEvent, ChartNote } from '../src/types'; + +const tap = (lane: number): ChartNote => ({ + lane, + type: 'tap', + endTimeSec: null, + endBeat: null, + source: 'synthetic-test', + confidence: 1, + foot: null, +}); + +// Generated fixture: 120 BPM, zero offset, and deliberately simple event times. +// It is not copied or measured from any user song or reference corpus. +export const referenceExcerptEvents: ChartEvent[] = [ + { + timeSec: 1, + beat: 2, + measure: 0, + subdivision: 4, + rowIndex: 2, + notes: [tap(0)], + sourceEventId: null, + pattern: null, + }, + { + timeSec: 2, + beat: 4, + measure: 1, + subdivision: 4, + rowIndex: 0, + notes: [{ + lane: 4, + type: 'hold', + endTimeSec: 2.5, + endBeat: 5, + source: 'synthetic-test', + confidence: 1, + foot: null, + }], + sourceEventId: null, + pattern: null, + }, + { + timeSec: 6, + beat: 12, + measure: 3, + subdivision: 4, + rowIndex: 0, + notes: [tap(0), tap(2)], + sourceEventId: null, + pattern: null, + }, +]; + +export const referenceExcerptChart: ChartDocument = { + id: 'synthetic-reference-chart', + title: 'Synthetic reference chart', + artist: 'BeatForge Test Lab', + music: 'synthetic-reference.wav', + sourceGroup: 'SPEED_CLUB', + sourcePath: 'SPEED_CLUB/synthetic-reference/synthetic-reference_Lv5.sm', + mode: 'pump-single', + laneCount: 5, + difficulty: 'Hard', + meter: 5, + bpm: 120, + offsetSec: 0, + durationSec: 20, + measureCount: 10, + tempoMap: [{ beat: 0, bpm: 120, timeSec: 0 }], + events: referenceExcerptEvents, + statistics: null, + validation: null, + optimization: null, + modelProvenance: null, + generator: 'synthetic_test_fixture', + generatorVersion: '1.0', + seed: null, +}; diff --git a/docs/AI_CHART_ENGINE.md b/docs/AI_CHART_ENGINE.md new file mode 100644 index 0000000..2dcf74b --- /dev/null +++ b/docs/AI_CHART_ENGINE.md @@ -0,0 +1,160 @@ +# BeatForge AI Chart Engine + +AI Chart Engine 是 BeatForge Studio 的全本地五轨制谱工作流。它直接读取本地授权语料中 +`SPEED_CLUB`、`SPEED_DEVIL` 与 `SPEED_REMIX` 的原始 `.sm` 和配套音频,以现有 +BeatForge 声学候选为时间锚点,生成、优化、验证并导出可玩的 `pump-single` 谱面。 + +运行时不调用 LLM 或云端 API。语料、特征、模型 checkpoint 和生成结果全部保存在本机, +且 `local-data/`、兼容旧目录 `材料/` 与 `storage/` 都不应提交到 Git。 + +## 本地参考语料 + +默认语料目录是: + +```text +local-data/speed-corpus/ +├── SPEED_CLUB/ +├── SPEED_DEVIL/ +└── SPEED_REMIX/ +``` + +谱面、模式和歌曲数量取决于开发者自行配置并获授权的本地语料。解析器保留变速 BPM 表、 +Offset、任意行细分、Tap、Hold 与 Mine,并能恢复源文件中出现的孤立 Hold tail。 + +如果语料在其他位置,通过 `.env` 指定: + +```dotenv +BEATFORGE_SPEED_CHARTS_DIR=/absolute/path/to/licensed-speed-corpus +``` + +检查解析后的语料统计: + +```bash +.venv/bin/python scripts/chart_engine.py inventory +``` + +## 本地训练数据集 + +模型只训练五轨谱面。下面的命令对语料中的 `pump-single` 谱面逐首运行生产版 BeatForge +分析器,并在 `storage/chart-engine/dataset/` 生成完整训练三元组: + +```bash +.venv/bin/python scripts/chart_engine.py build-dataset \ + --mode pump-single \ + --analysis-mode balanced \ + --analyze-missing +``` + +首次运行会分析真实音频,耗时取决于 CPU/GPU;以后会按音频 SHA-256 复用 +`.feature-cache/`。同一音频的不同谱面使用相同的 train/validation/test split,避免音频泄漏。 +未带 `--analyze-missing` 时,缺少真实分析的样本会被明确记录为跳过,不会生成空候选或占位特征。 +如果 MP3 能读取元数据但完整 libsndfile 解码失败,构建器会自动使用本地 FFmpeg 解码为临时 +PCM 再运行同一个分析器,并在分析 JSON 中记录 `source_decode_backend`;临时音频随后删除。 + +每个样本目录包含: + +```text +/ +├── audio.mp3 原始真实音频的硬链接、符号链接或本地副本 +├── beatforge.json 生产版 BeatForge 分析与候选事件 +├── chart.json 解析后的真实 SM 目标 +└── metadata.json 来源、难度、哈希、split 与 realData 标记 +``` + +数据集根目录还包含 `manifest.json`、`build_report.json` 与 `chart_statistics.json`。 + +## 本地 Transformer + +安装可选 PyTorch 依赖后训练: + +```bash +.venv/bin/pip install -e 'apps/api[chart-ml]' + +.venv/bin/python scripts/train_chart_model.py \ + --dataset storage/chart-engine/dataset \ + --output storage/chart-engine/models/chart-transformer.pt \ + --epochs 12 \ + --batch-size 8 \ + --sequence-length 512 \ + --device auto +``` + +模型是带难度条件的 Transformer encoder。输入为当前歌曲真实 BeatForge candidate 序列,输出 +五个独立面板概率和 Hold 概率。Checkpoint 记录固定特征 schema、归一化参数、数据集指纹、 +真实样本来源、训练/验证损失、Torch 版本和设备,不包含音频本体。 + +当 `chart-transformer.pt` 存在时,生成接口默认使用它;checkpoint 不存在时会明确回退到基于 +本地参考语料统计的确定性规则生成器。请求传入 `useLocalModel: false` 可主动关闭模型。损坏或 +不兼容的 checkpoint 会返回 `LOCAL_CHART_MODEL_FAILED`,不会静默伪装成模型结果。 + +## 生成、优化与验证 + +生成器把 BeatForge `accepted` candidate 与持久化 hitPoint 当作节奏骨架;模型的五轨概率只参与 +选键和 Hold,不再被误当成“事件是否存在”的概率。可选的 uncertain/rejected candidate 才会接受 +模型阈值和难度预算筛选。同一量化槽只生成一个时序事件,同时在 `sourceEventIds`、 +`sourceHitPointIds` 中保留合并前的完整来源,`sourceEventId` 继续作为兼容主 ID。 + +节奏网格随难度分级:Lv.1–3 为 1/4,Lv.4–7 为 1/8,Lv.8–10 最高为 1/16;只有 Lv.11–15 +能根据低网格置信度的真实声学位置混合 1/16 与 1/24。连续 1/16 密集段保留为高压单键流, +随机或模型双押必须同时满足前后间距,不能用一个双押冒充两个参考标记。`enableSpin` 是独立开关, +默认关闭,开启后可插入三键小圈与五键大圈。 + +确定性优化器在密度超限时先把可选双押降为单键、再删除低置信可选事件;accepted marker 与 +hitPoint 的时序行不会被静默删除。优化器和验证器共享按真实 BPM 校准的两秒整数 note 容量, +连续 1/16 可以通过,双押和持续 1/24 仍按每个 note 单独计数。验证器还会复核难度允许的实际 +细分、同脚 16 分连续、身体位移、多键同时踩踏和 Hold 生命周期,并返回 0–100 分与逐项问题。 + +## API + +本地授权语料: + +- `GET /api/chart-engine/reference-charts`:筛选五轨/十轨、分组与标题。 +- `GET /api/chart-engine/reference-charts/{chartId}`:读取完整绝对时间谱面。 +- `GET|HEAD /api/chart-engine/reference-charts/{chartId}/audio`:支持 HTTP Range 的真实音频。 +- `GET /api/chart-engine/statistics`:BPM、NPS、动作、脚法、转圈与轨道转移统计。 + +项目歌曲: + +- `POST /api/tracks/{trackId}/chart/generate`:生成并验证谱面。 +- `GET /api/tracks/{trackId}/chart/latest`:读取最近生成结果。 +- `GET /api/tracks/{trackId}/chart/export?generationId=...`:导出 UTF-8 BOM `.sm`。 + +生成请求示例: + +```json +{ + "difficulty": 10, + "enableSpin": false, + "useLocalModel": true, + "seed": 20260721 +} +``` + +响应中的 `chart.generator` 明确区分 `local_chart_transformer` 与规则生成器, +`chart.modelProvenance` 记录模型数据集指纹、checkpoint SHA-256 与 `realDataOnly` 标记; +`chart.optimization` 同时记录 accepted/hitPoint 锚点的输入输出行数,因此不同权重不会覆盖同一 +生成版本,参考点覆盖也可逐次审计。 + +## 前端工作区 + +启动开发环境后: + +- `/chart-engine` 浏览本地五轨参考谱面、语料统计与同步播放器。 +- `/projects/{projectId}/chart` 为项目歌曲生成 1–15 难度谱面、切换转圈、查看验证结果并导出 SM。 + +Canvas 使用绝对 `timeSec` 绘制五条面板,音符自下向上接近判定线;HTML Audio 是唯一时钟, +播放、暂停、前后跳转和拖动进度会保持音频与谱面一致。 + +## 本地存储 + +```text +storage/chart-engine/ +├── dataset/ 真实训练三元组、缓存与清单 +├── models/ +│ └── chart-transformer.pt 本地模型 checkpoint +└── generated/ + └── / 版本化 JSON 与 latest.json +``` + +这些文件可由真实素材重新构建,默认不进入版本控制。分享训练产物或导出的 SM 前,请确认拥有 +相应音频与谱面的使用权。 diff --git a/scripts/chart_engine.py b/scripts/chart_engine.py new file mode 100644 index 0000000..951b453 --- /dev/null +++ b/scripts/chart_engine.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +"""Build and inspect the local real-data BeatForge chart engine.""" + +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(PROJECT_ROOT / "apps" / "api")) + +from beatforge_api.chart_engine.dataset import build_dataset # noqa: E402 +from beatforge_api.chart_engine.library import ReferenceLibrary # noqa: E402 +from beatforge_api.config import get_settings # noqa: E402 + + +def _parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description=__doc__) + subparsers = parser.add_subparsers(dest="command", required=True) + subparsers.add_parser("inventory", help="Inspect the local SPEED reference corpus") + + build = subparsers.add_parser( + "build-dataset", help="Build complete real training triples" + ) + build.add_argument("--output", type=Path) + build.add_argument( + "--mode", choices=("pump-single", "pump-double"), default="pump-single" + ) + build.add_argument( + "--analysis-mode", + choices=("recall", "balanced", "clean", "accurate"), + default="balanced", + ) + build.add_argument("--analyze-missing", action="store_true") + build.add_argument("--limit", type=int) + return parser + + +def main() -> int: + args = _parser().parse_args() + settings = get_settings() + library = ReferenceLibrary(settings.speed_charts_dir) + if args.command == "inventory": + stats = library.statistics() + print( + json.dumps( + stats.model_dump(by_alias=True, mode="json"), + ensure_ascii=False, + indent=2, + ) + ) + return 0 + report = build_dataset( + library, + args.output or settings.chart_dataset_dir, + mode=args.mode, + analyze_missing=args.analyze_missing, + analysis_mode=args.analysis_mode, + limit=args.limit, + ) + print(json.dumps(report.to_dict(), ensure_ascii=False, indent=2)) + return 0 if report.failed == 0 else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/train_chart_model.py b/scripts/train_chart_model.py new file mode 100644 index 0000000..7704165 --- /dev/null +++ b/scripts/train_chart_model.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +"""Train the local BeatForge five-lane Transformer from completed real dataset triples.""" + +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(PROJECT_ROOT / "apps" / "api")) + +from beatforge_api.chart_engine.learning import ( # noqa: E402 + FEATURE_NAMES, + TrainingConfig, + train_chart_transformer, +) +from beatforge_api.chart_engine.model import ChartTransformerConfig # noqa: E402 +from beatforge_api.config import get_settings # noqa: E402 + + +def _parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--dataset", type=Path) + parser.add_argument("--output", type=Path) + parser.add_argument("--epochs", type=int, default=12) + parser.add_argument("--batch-size", type=int, default=8) + parser.add_argument("--learning-rate", type=float, default=3e-4) + parser.add_argument("--weight-decay", type=float, default=1e-4) + parser.add_argument("--sequence-length", type=int, default=512) + parser.add_argument("--sequence-stride", type=int) + parser.add_argument("--match-tolerance-ms", type=float, default=80.0) + parser.add_argument("--seed", type=int, default=20260721) + parser.add_argument("--device", default="auto") + parser.add_argument("--no-validation", action="store_true") + parser.add_argument("--max-batches-per-epoch", type=int) + parser.add_argument("--d-model", type=int, default=96) + parser.add_argument("--heads", type=int, default=4) + parser.add_argument("--layers", type=int, default=3) + parser.add_argument("--feedforward", type=int, default=256) + parser.add_argument("--dropout", type=float, default=0.1) + return parser + + +def main() -> int: + args = _parser().parse_args() + settings = get_settings() + dataset = (args.dataset or settings.chart_dataset_dir).expanduser().resolve() + output = ( + (args.output or settings.chart_models_dir / "chart-transformer.pt") + .expanduser() + .resolve() + ) + training = TrainingConfig( + epochs=args.epochs, + batch_size=args.batch_size, + learning_rate=args.learning_rate, + weight_decay=args.weight_decay, + match_tolerance_ms=args.match_tolerance_ms, + sequence_length=args.sequence_length, + sequence_stride=args.sequence_stride, + seed=args.seed, + device=args.device, + validation_split=None if args.no_validation else "validation", + max_batches_per_epoch=args.max_batches_per_epoch, + ) + model = ChartTransformerConfig( + input_dim=len(FEATURE_NAMES), + d_model=args.d_model, + nhead=args.heads, + num_layers=args.layers, + dim_feedforward=args.feedforward, + dropout=args.dropout, + max_sequence_length=args.sequence_length, + ) + result = train_chart_transformer( + dataset, + output, + training=training, + model_config=model, + ) + print(json.dumps(result.to_dict(), ensure_ascii=False, indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())