From 46050ead95d39491b4b20ceca572228e809c8713 Mon Sep 17 00:00:00 2001 From: cmander02 <15946840899@163.com> Date: Tue, 28 Jul 2026 11:15:22 +0800 Subject: [PATCH 01/25] feat: synchronize remote tasks and portable archives --- .gitignore | 5 +- backend/app/api/routes/pipeline.py | 522 ++- backend/app/api/routes/settings.py | 1 + backend/app/api/routes/sync.py | 537 +++ backend/app/api/routes/tasks.py | 84 +- backend/app/cli/main.py | 23 +- backend/app/core/archive_sync.py | 612 +++ backend/app/core/database.py | 1071 ++++- backend/app/core/model_router.py | 7 +- backend/app/core/pipeline.py | 7 +- backend/app/core/queue.py | 134 +- backend/app/core/settings.py | 29 +- backend/app/main.py | 65 +- backend/app/models/task.py | 16 +- backend/app/services/analysis/vlm.py | 2 +- backend/app/services/archiving/thumbnails.py | 218 +- backend/app/services/remote_sync.py | 1172 ++++++ pyproject.toml | 1 + tests/test_backend_api_smoke.py | 191 + tests/test_model_router_multimodal_kb.py | 2 +- tests/test_pipeline_model_bindings.py | 4 +- tests/test_remote_sync.py | 954 +++++ tests/test_remote_sync_client.py | 900 +++++ tests/test_thumbnail_helpers.py | 221 ++ tests/test_uvr_pipeline_guards.py | 9 +- tests/test_vlm_service.py | 76 + uv.lock | 3675 ++++++++++++++++++ 27 files changed, 10387 insertions(+), 151 deletions(-) create mode 100644 backend/app/api/routes/sync.py create mode 100644 backend/app/core/archive_sync.py create mode 100644 backend/app/services/remote_sync.py create mode 100644 tests/test_remote_sync.py create mode 100644 tests/test_remote_sync_client.py create mode 100644 tests/test_thumbnail_helpers.py create mode 100644 tests/test_vlm_service.py create mode 100644 uv.lock diff --git a/.gitignore b/.gitignore index a1bc2fb..95eb398 100644 --- a/.gitignore +++ b/.gitignore @@ -40,10 +40,10 @@ htmlcov/ .ruff_cache/ # uv -uv.lock +# Track uv.lock for reproducible application, server, and release builds. # Data directories (processing files, media, outputs) -data/ +/data/ *.wav *.mp3 *.mp4 @@ -65,6 +65,7 @@ nul # doc/ .claude/ +.codex/ # Experiment outputs (frames, crops, results) experiment/*/out/ diff --git a/backend/app/api/routes/pipeline.py b/backend/app/api/routes/pipeline.py index ef428a8..aa7cd19 100644 --- a/backend/app/api/routes/pipeline.py +++ b/backend/app/api/routes/pipeline.py @@ -1,41 +1,104 @@ """Direct pipeline operation routes.""" import asyncio +import hashlib +import ipaddress import json import logging import re import shutil import subprocess import urllib.request +from datetime import datetime +from email.utils import formatdate from pathlib import Path +from typing import Any +from urllib.parse import urlparse -from fastapi import APIRouter, HTTPException, UploadFile, File, Form +import httpx +from fastapi import APIRouter, File, HTTPException, Query, Request, Response, UploadFile from fastapi.responses import FileResponse from pydantic import BaseModel -from typing import Any - -# Windows reserved device names (case-insensitive, with or without extension) -_WIN_RESERVED = re.compile( - r'^(CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9])(\..+)?$', re.IGNORECASE -) -import ipaddress -from urllib.parse import urlparse - -from app.core.settings import get_runtime_settings +from app.core.network import httpx_client_kwargs, urllib_urlopen from app.core.pipeline import pipeline_steps_schema +from app.core.settings import get_runtime_settings from app.core.source_normalization import normalize_source_input -from app.core.network import urllib_urlopen from app.services.archiving.thumbnails import ( + ArchiveImageFormatError, + ArchiveImageTooLargeError, + PillowUnavailableError, create_image_thumbnail, create_image_thumbnail_from_bytes, first_image_note_image, image_media_type, + resize_archive_image_to_jpeg, +) + +# Windows reserved device names (case-insensitive, with or without extension) +_WIN_RESERVED = re.compile( + r'^(CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9])(\..+)?$', re.IGNORECASE ) logger = logging.getLogger(__name__) +async def _proxy_remote_mirror_mutation( + archive_dir: Path, + endpoint: str, + payload: dict[str, Any], +) -> dict[str, Any] | None: + """Apply archive mutations to the coordinator that owns a local mirror.""" + from app.core.database import get_task_store + + task = get_task_store().find_by_output_dir(archive_dir) + result = task.result if task and isinstance(task.result, dict) else {} + sync_info = ( + result.get("remote_sync") + if isinstance(result.get("remote_sync"), dict) + else {} + ) + if not sync_info.get("mirror"): + return None + + remote_path = str(sync_info.get("remote_output_dir") or "").strip() + settings = get_runtime_settings() + server_url = str(settings.remote_server_url or "").strip().rstrip("/") + if not remote_path or not settings.remote_sync_enabled or not server_url: + raise HTTPException(409, "Remote mirror coordinator is not configured") + + request_payload = {**payload, "path": remote_path} + headers = {"X-Requested-With": "MPP-EXE-Remote-Mutation"} + if settings.remote_api_token: + headers["Authorization"] = f"Bearer {settings.remote_api_token}" + url = f"{server_url}/api/pipeline/archives/{endpoint.lstrip('/')}" + try: + async with httpx.AsyncClient( + timeout=httpx.Timeout(360.0, connect=15.0), + follow_redirects=False, + **httpx_client_kwargs(url), + ) as client: + response = await client.post(url, json=request_payload, headers=headers) + except httpx.RequestError as exc: + raise HTTPException(502, "Cannot reach the remote archive coordinator") from exc + if response.status_code >= 400: + try: + detail = response.json().get("detail") + except (ValueError, AttributeError): + detail = None + raise HTTPException( + response.status_code, + str(detail or f"Remote archive mutation failed ({response.status_code})"), + ) + try: + body = response.json() + except ValueError as exc: + raise HTTPException(502, "Remote archive coordinator returned invalid JSON") from exc + if not isinstance(body, dict): + raise HTTPException(502, "Remote archive coordinator returned invalid data") + return body + + def _validate_url(url: str) -> None: """Validate URL to prevent SSRF — reject file://, internal IPs, localhost.""" parsed = urlparse(url) @@ -78,6 +141,12 @@ class AnalyzeRequest(BaseModel): text: str +class ArchivePolishRequest(BaseModel): + path: str + text: str | None = None + source_filename: str | None = None + + class XiaohongshuLoginRequest(BaseModel): timeout_sec: int = 180 @@ -89,6 +158,20 @@ class XiaohongshuLoginRequest(BaseModel): _THUMBNAIL_MAX_BYTES = 12 * 1024 * 1024 _RAW_IMAGE_FALLBACK_MAX_BYTES = 1024 * 1024 +_ARCHIVE_IMAGE_CACHE_SECONDS = 24 * 60 * 60 +_ARCHIVE_INTERNAL_ROOTS = { + "_staging", + "_remote_sync", + "_sync_downloads", + "_remote_sync_client", +} +_ARCHIVE_MARKER_FILES = { + "metadata.json", + "source.md", + "summary.md", + "transcript.srt", + "transcript_polished.srt", +} def _read_archive_metadata(archive_dir: Path) -> dict[str, Any]: @@ -149,6 +232,122 @@ def _can_return_raw_image(path: Path) -> bool: return False +def _resolve_archive_image(path: str) -> Path: + """Resolve a supported image belonging to a flat data_root archive.""" + candidate = Path(path) + try: + resolved = candidate.resolve(strict=True) + except (OSError, RuntimeError): + raise HTTPException(404, "Archive image not found") + + data_root = Path(get_runtime_settings().data_root).resolve() + try: + relative = resolved.relative_to(data_root) + except ValueError: + raise HTTPException(403, "Cannot access paths outside data directory") + + if ( + len(relative.parts) < 2 + or relative.parts[0].startswith(".") + or relative.parts[0] in _ARCHIVE_INTERNAL_ROOTS + ): + raise HTTPException(403, "Path does not belong to an archive") + + archive_dir = data_root / relative.parts[0] + if not archive_dir.is_dir() or not any( + (archive_dir / marker).is_file() + for marker in _ARCHIVE_MARKER_FILES + ): + raise HTTPException(403, "Path does not belong to an archive") + if not resolved.is_file(): + raise HTTPException(404, "Archive image not found") + if resolved.suffix.lower() not in { + ".avif", + ".bmp", + ".gif", + ".jpeg", + ".jpg", + ".png", + ".webp", + }: + raise HTTPException(415, "Unsupported archive image type") + return resolved + + +def _archive_image_response_headers( + source: Path, + *, + max_edge: int, + quality: int, +) -> dict[str, str]: + stat = source.stat() + fingerprint = hashlib.sha256( + f"{stat.st_mtime_ns}:{stat.st_size}:{max_edge}:{quality}".encode("ascii"), + ).hexdigest() + return { + "Cache-Control": f"private, max-age={_ARCHIVE_IMAGE_CACHE_SECONDS}", + "Content-Disposition": 'inline; filename="archive-image.jpg"', + "ETag": f'"{fingerprint}"', + "Last-Modified": formatdate(stat.st_mtime, usegmt=True), + "Vary": "Authorization", + "X-Content-Type-Options": "nosniff", + } + + +def _etag_matches(if_none_match: str | None, etag: str) -> bool: + if not if_none_match: + return False + return any( + candidate.strip() in {"*", etag} + for candidate in if_none_match.split(",") + ) + + +def _generate_video_thumbnail(video_file: Path, archive_dir: Path) -> Path | None: + thumb_path = archive_dir / "thumbnail.jpg" + ffmpeg = shutil.which("ffmpeg") + if not ffmpeg: + logger.warning("ffmpeg thumbnail failed: ffmpeg executable not found") + return None + + try: + result = subprocess.run( + [ + ffmpeg, + "-y", + "-ss", + "3", + "-i", + str(video_file), + "-vframes", + "1", + "-vf", + "scale=480:-2", + "-q:v", + "5", + str(thumb_path), + ], + capture_output=True, + timeout=15, + check=False, + ) + if result.returncode == 0 and thumb_path.is_file() and thumb_path.stat().st_size > 0: + return thumb_path + logger.warning( + "ffmpeg thumbnail failed: returncode=%s stderr=%s", + result.returncode, + result.stderr.decode("utf-8", errors="replace").strip(), + ) + except Exception as exc: + logger.warning("ffmpeg thumbnail failed: %s", exc) + + try: + thumb_path.unlink(missing_ok=True) + except OSError: + pass + return None + + @router.get("/steps") async def get_pipeline_steps(): """Return canonical pipeline step ids, names, and order.""" @@ -255,10 +454,32 @@ def sweep_stale_staging(max_age_hours: float = 24.0) -> int: root = _staging_root() if not root.exists(): return 0 + protected_dirs: set[Path] = set() + try: + from app.core.database import get_task_store + + active_tasks = get_task_store().list_by_statuses( + ["pending", "queued", "processing", "paused"] + ) + resolved_root = root.resolve() + for task in active_tasks: + source = str(task.source or "") + if source.startswith(("http://", "https://", "upload://")): + continue + try: + relative = Path(source).resolve().relative_to(resolved_root) + except (OSError, ValueError): + continue + if relative.parts: + protected_dirs.add((resolved_root / relative.parts[0]).resolve()) + except Exception: + logger.warning("Unable to resolve active staged inputs during cleanup", exc_info=True) cutoff = time.time() - max_age_hours * 3600 removed = 0 for entry in root.iterdir(): try: + if entry.resolve() in protected_dirs: + continue if entry.is_dir() and entry.stat().st_mtime < cutoff: shutil.rmtree(entry, ignore_errors=True) removed += 1 @@ -352,6 +573,113 @@ async def polish(req: AnalyzeRequest): return {"polished": await polish_text(req.text)} +@router.post("/archives/polish") +async def polish_archive(req: ArchivePolishRequest): + """Run an additional polish pass and persist it as a portable archive artifact.""" + archive_dir = Path(req.path) + if not archive_dir.is_dir(): + raise HTTPException(404, "Archive directory not found") + + data_root = Path(get_runtime_settings().data_root).resolve() + try: + archive_dir = archive_dir.resolve() + archive_dir.relative_to(data_root) + except ValueError: + raise HTTPException(403, "Cannot modify paths outside data directory") + + source_name = (req.source_filename or "").strip() + if source_name and ( + Path(source_name).name != source_name + or "/" in source_name + or "\\" in source_name + ): + raise HTTPException(400, "Invalid source filename") + + proxied = await _proxy_remote_mirror_mutation( + archive_dir, + "polish", + { + "text": req.text, + "source_filename": source_name or None, + }, + ) + if proxied is not None: + return proxied + + text = req.text + if text is None: + candidates = [ + source_name, + "transcript_polished.md", + "transcript_raw.md", + "transcript.srt", + "content.md", + "source.md", + ] + for filename in candidates: + if not filename: + continue + candidate = archive_dir / filename + if candidate.is_file(): + text = candidate.read_text(encoding="utf-8") + source_name = filename + break + + content = (text or "").strip() + if not content: + raise HTTPException(400, "No text is available for additional polishing") + if len(content) > 2_000_000: + raise HTTPException(413, "Text is too large for additional polishing") + + metadata = _read_archive_metadata(archive_dir) + from app.services.analysis import polish_text + + polished = await polish_text( + content, + context={ + "title": str(metadata.get("title") or ""), + "additional_pass": True, + }, + ) + output_name = "transcript_extra_polished.md" + output_path = archive_dir / output_name + temp_path = archive_dir / f".{output_name}.tmp" + temp_path.write_text(polished, encoding="utf-8") + temp_path.replace(output_path) + + metadata["extra_polish_file"] = output_name + metadata["extra_polish_source"] = source_name or "request" + metadata["extra_polished_at"] = datetime.now().isoformat() + metadata_path = archive_dir / "metadata.json" + metadata_temp = archive_dir / ".metadata.json.extra-polish.tmp" + metadata_temp.write_text( + json.dumps(metadata, ensure_ascii=False, indent=2), + encoding="utf-8", + ) + metadata_temp.replace(metadata_path) + + from app.core.database import get_task_store + + store = get_task_store() + task = store.find_by_output_dir(archive_dir) + updated_task = None + if task: + updated_task = store.touch_sync_revision( + task.id, + result_updates={ + "extra_polish_file": output_name, + "extra_polished_at": metadata["extra_polished_at"], + }, + ) + + return { + "polished": polished, + "filename": output_name, + "path": str(output_path), + "sync_revision": updated_task.sync_revision if updated_task else None, + } + + @router.post("/summarize") async def summarize(req: AnalyzeRequest): """Generate summary.""" @@ -406,38 +734,40 @@ async def delete_archive(req: ArchiveDeleteRequest): if not archive_dir.is_dir(): raise HTTPException(404, "Archive directory not found") - # Safety: only allow deleting within data_root + # Only direct, registered archive directories may be removed. Internal + # staging/synchronization trees and data_root itself are never archives. rt = get_runtime_settings() data_root = Path(rt.data_root).resolve() + resolved_archive = archive_dir.resolve() try: - archive_dir.resolve().relative_to(data_root) + relative_archive = resolved_archive.relative_to(data_root) except ValueError: raise HTTPException(403, "Cannot delete paths outside data directory") + internal_roots = { + "_staging", + "_remote_sync", + "_sync_downloads", + "_remote_sync_client", + } + if ( + resolved_archive == data_root + or len(relative_archive.parts) != 1 + or relative_archive.parts[0] in internal_roots + or relative_archive.parts[0].startswith(".") + or archive_dir.is_symlink() + ): + raise HTTPException(403, "Path is not a deletable archive directory") + + from app.core.database import get_task_store - # Try to find and delete the associated task record by matching output_dir - task_deleted = False - from uuid import UUID - from app.core.database import get_task_store, _get_conn store = get_task_store() - conn = _get_conn() - archive_dir_str = str(archive_dir.resolve()) - # Search for task whose result JSON contains this output_dir path - rows = conn.execute("SELECT id, status, result FROM tasks WHERE result IS NOT NULL").fetchall() - for row in rows: - try: - result = json.loads(row["result"]) if isinstance(row["result"], str) else row["result"] - if result and str(Path(result.get("output_dir", "")).resolve()) == archive_dir_str: - if str(row["status"]) in {"queued", "processing"}: - raise HTTPException(409, "Archive directory is used by an active task") - store.delete(UUID(row["id"])) - task_deleted = True - break - except HTTPException: - raise - except (json.JSONDecodeError, TypeError, ValueError): - continue + task = store.find_by_output_dir(resolved_archive) + if task is None: + raise HTTPException(404, "Archive is not associated with a registered task") + if str(task.status) not in {"completed", "failed", "cancelled"}: + raise HTTPException(409, "Archive directory is used by an active task") - # Delete the archive directory (with Windows file-lock retry) + # Remove files first so a filesystem error leaves the task record intact. import time def _onerror_retry(func, path, exc_info): @@ -456,7 +786,7 @@ def _onerror_retry(func, path, exc_info): last_err = None for attempt in range(3): try: - shutil.rmtree(archive_dir, onerror=_onerror_retry) + shutil.rmtree(resolved_archive, onerror=_onerror_retry) break except Exception as e: last_err = e @@ -464,19 +794,25 @@ def _onerror_retry(func, path, exc_info): time.sleep(0.5) else: # If folder still exists but is now empty, that's ok - if archive_dir.exists() and any(archive_dir.iterdir()): + if resolved_archive.exists() and any(resolved_archive.iterdir()): raise HTTPException(500, f"Failed to delete archive: {last_err}") # Empty dir or gone — try final rmdir try: - archive_dir.rmdir() + resolved_archive.rmdir() except Exception: pass - logger.info(f"Deleted archive: {archive_dir} (task={task_deleted})") + if resolved_archive.exists(): + raise HTTPException(500, "Failed to delete archive directory") + task_deleted = store.delete(task.id) + if not task_deleted: + raise HTTPException(500, "Archive removed but task record could not be deleted") + + logger.info(f"Deleted archive: {resolved_archive} (task={task_deleted})") return { "message": "Deleted", - "path": str(archive_dir), + "path": str(resolved_archive), "task_deleted": task_deleted, } @@ -504,6 +840,14 @@ async def rename_archive(req: ArchiveRenameRequest): if not new_title: raise HTTPException(400, "Title cannot be empty") + proxied = await _proxy_remote_mirror_mutation( + archive_dir.resolve(), + "rename", + {"title": new_title}, + ) + if proxied is not None: + return proxied + meta_path = archive_dir / "metadata.json" meta = {} if meta_path.exists(): @@ -515,7 +859,71 @@ async def rename_archive(req: ArchiveRenameRequest): meta["title"] = new_title meta_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8") - return {"success": True, "title": new_title} + from app.core.database import get_task_store + + store = get_task_store() + task = store.find_by_output_dir(archive_dir) + updated_task = None + if task: + result_metadata = ( + dict(task.result.get("metadata") or {}) + if isinstance(task.result, dict) + and isinstance(task.result.get("metadata"), dict) + else {} + ) + result_metadata["title"] = new_title + updated_task = store.touch_sync_revision( + task.id, + result_updates={"metadata": result_metadata}, + ) + + return { + "success": True, + "title": new_title, + "sync_revision": updated_task.sync_revision if updated_task else None, + } + + +@router.get("/archives/image") +async def archive_image( + request: Request, + path: str, + max_edge: int = Query(default=1280, ge=256, le=4096), + quality: int = Query(default=84, ge=55, le=90), +): + """Return a bounded JPEG rendition of one image stored in an archive.""" + source = _resolve_archive_image(path) + try: + headers = _archive_image_response_headers( + source, + max_edge=max_edge, + quality=quality, + ) + except OSError: + raise HTTPException(404, "Archive image not found") + + if _etag_matches(request.headers.get("if-none-match"), headers["ETag"]): + return Response(status_code=304, headers=headers) + + try: + payload = await asyncio.to_thread( + resize_archive_image_to_jpeg, + source, + max_edge=max_edge, + quality=quality, + ) + except ArchiveImageTooLargeError as exc: + raise HTTPException(413, str(exc)) from exc + except ArchiveImageFormatError as exc: + raise HTTPException(415, str(exc)) from exc + except PillowUnavailableError as exc: + raise HTTPException(503, str(exc)) from exc + + return Response( + content=payload, + media_type="image/jpeg", + headers=headers, + ) @router.get("/archives/thumbnail") @@ -544,7 +952,7 @@ async def archive_thumbnail(path: str): for candidate in ["cover.jpg", "cover.png", "cover.webp"]: cover = archive_dir / candidate if cover.exists(): - thumb = create_image_thumbnail(cover, archive_dir) + thumb = await asyncio.to_thread(create_image_thumbnail, cover, archive_dir) if thumb: return FileResponse(thumb, media_type="image/jpeg") if _can_return_raw_image(cover): @@ -553,7 +961,7 @@ async def archive_thumbnail(path: str): first_image = first_image_note_image(archive_dir) if first_image: - thumb = create_image_thumbnail(first_image, archive_dir) + thumb = await asyncio.to_thread(create_image_thumbnail, first_image, archive_dir) if thumb: return FileResponse(thumb, media_type="image/jpeg") if _can_return_raw_image(first_image): @@ -562,7 +970,7 @@ async def archive_thumbnail(path: str): meta = _read_archive_metadata(archive_dir) remote_thumb = _first_thumbnail_url(meta) if remote_thumb: - thumb = _cache_remote_thumbnail(remote_thumb, archive_dir) + thumb = await asyncio.to_thread(_cache_remote_thumbnail, remote_thumb, archive_dir) if thumb: return FileResponse(thumb, media_type="image/jpeg") @@ -587,26 +995,10 @@ async def archive_thumbnail(path: str): if not video_file: raise HTTPException(404, "No thumbnail or video found") - # Generate thumbnail via ffmpeg - thumb_path = archive_dir / "thumbnail.jpg" - try: - subprocess.run( - [ - "ffmpeg", "-y", "-ss", "3", "-i", str(video_file), - "-vframes", "1", "-vf", "scale=480:-2", - "-q:v", "5", str(thumb_path), - ], - capture_output=True, - timeout=15, - ) - except Exception as e: - logger.warning(f"ffmpeg thumbnail failed: {e}") - raise HTTPException(500, "Thumbnail generation failed") - - if thumb_path.exists(): - return FileResponse(thumb_path, media_type="image/jpeg") - - raise HTTPException(500, "Thumbnail generation produced no output") + thumb = await asyncio.to_thread(_generate_video_thumbnail, video_file, archive_dir) + if thumb: + return FileResponse(thumb, media_type="image/jpeg") + raise HTTPException(500, "Thumbnail generation failed") @router.post("/cleanup/{task_id}") diff --git a/backend/app/api/routes/settings.py b/backend/app/api/routes/settings.py index 3becc46..b41d856 100644 --- a/backend/app/api/routes/settings.py +++ b/backend/app/api/routes/settings.py @@ -25,6 +25,7 @@ # Fields that contain secrets — values are masked in GET responses _SECRET_FIELDS = { "api_token", + "remote_api_token", "anthropic_api_key", "openai_api_key", "custom_api_key", diff --git a/backend/app/api/routes/sync.py b/backend/app/api/routes/sync.py new file mode 100644 index 0000000..04b6490 --- /dev/null +++ b/backend/app/api/routes/sync.py @@ -0,0 +1,537 @@ +"""Coordinator routes for EXE workers and portable archive synchronization.""" + +from __future__ import annotations + +import asyncio +import json +import re +import shutil +import zipfile +from datetime import datetime, timezone +from pathlib import Path +from typing import Any +from uuid import UUID, uuid4 + +from fastapi import APIRouter, File, Form, Header, HTTPException, Query, UploadFile +from fastapi.responses import FileResponse +from pydantic import BaseModel, Field +from starlette.background import BackgroundTask + +from app.core.archive_sync import ( + MAX_UPLOAD_BYTES, + MAX_PORTABLE_RESULT_JSON_BYTES, + archive_file_manifest, + build_archive_zip, + publish_archive, + rewrite_archive_image_paths, + rewrite_result_image_paths, + safe_extract_zip, + sanitize_portable_result, + stream_upload_to_path, +) +from app.core.database import get_task_store +from app.core.events import TaskEvent, get_event_bus +from app.core.settings import get_runtime_settings +from app.models import TaskStatus + +router = APIRouter(tags=["sync"]) +_WORKER_ID_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$") + + +class WorkerRegisterRequest(BaseModel): + worker_id: str | None = Field(default=None, max_length=128) + name: str = Field(default="MPP EXE", min_length=1, max_length=128) + capabilities: dict[str, Any] = Field(default_factory=dict) + + +class WorkerHeartbeatRequest(BaseModel): + capabilities: dict[str, Any] | None = None + + +class WorkerClaimRequest(BaseModel): + lease_seconds: int = Field(default=900, ge=30, le=3600) + + +class LeaseRenewRequest(BaseModel): + lease_token: str = Field(min_length=16, max_length=512) + lease_seconds: int = Field(default=900, ge=30, le=3600) + + +class WorkerFailureRequest(BaseModel): + lease_token: str = Field(min_length=16, max_length=512) + error: str = Field(min_length=1, max_length=20_000) + + +def _validate_worker_id(worker_id: str) -> str: + value = worker_id.strip() + if not _WORKER_ID_PATTERN.fullmatch(value): + raise HTTPException(status_code=400, detail="Invalid worker_id") + return value + + +def _server_worker_record() -> dict[str, Any]: + now = datetime.now(timezone.utc).isoformat() + return { + "id": "server", + "name": "Server", + "executor": "server", + "status": "online", + "online": True, + "registered_at": None, + "last_seen_at": now, + "capabilities": {"executor": "server"}, + } + + +def _worker_api_record(record: dict[str, Any]) -> dict[str, Any]: + last_seen_raw = str(record.get("last_seen_at") or "") + try: + last_seen = datetime.fromisoformat(last_seen_raw) + if last_seen.tzinfo is None: + last_seen = last_seen.replace(tzinfo=timezone.utc) + online = (datetime.now(timezone.utc) - last_seen).total_seconds() <= 90 + except ValueError: + online = False + return { + **record, + "executor": "exe", + "status": "online" if online else "offline", + "online": online, + } + + +@router.get("/workers") +async def list_workers(): + """List selectable processing targets, including the built-in server.""" + workers = [_server_worker_record()] + workers.extend(_worker_api_record(record) for record in get_task_store().list_workers()) + return {"workers": workers} + + +@router.post("/workers/register") +async def register_worker(request: WorkerRegisterRequest): + worker_id = _validate_worker_id(request.worker_id) if request.worker_id else None + record = get_task_store().register_worker( + worker_id=worker_id, + name=request.name.strip(), + capabilities=request.capabilities, + ) + return {"worker": _worker_api_record(record)} + + +@router.post("/workers/{worker_id}/heartbeat") +async def heartbeat_worker(worker_id: str, request: WorkerHeartbeatRequest): + worker_id = _validate_worker_id(worker_id) + record = get_task_store().heartbeat_worker( + worker_id, + capabilities=request.capabilities, + ) + if not record: + raise HTTPException(status_code=404, detail="Worker is not registered") + return {"worker": _worker_api_record(record)} + + +@router.post("/workers/{worker_id}/claim") +async def claim_task(worker_id: str, request: WorkerClaimRequest): + worker_id = _validate_worker_id(worker_id) + if not get_task_store().get_worker(worker_id): + raise HTTPException(status_code=404, detail="Worker is not registered") + claimed = get_task_store().claim_remote_task( + worker_id, + lease_seconds=request.lease_seconds, + ) + if not claimed: + return {"task": None} + task = claimed["task"] + try: + input_path = _claimed_task_input_path( + task.id, + worker_id, + claimed["lease_token"], + ) + except HTTPException: + input_path = None + if input_path is not None: + claimed["input"] = { + "filename": input_path.name, + "size": input_path.stat().st_size, + } + await get_event_bus().publish( + TaskEvent( + task.id, + "claimed", + { + "executor": worker_id, + "attempt": claimed["attempt"], + "lease_expires_at": claimed["lease_expires_at"], + }, + ) + ) + return claimed + + +@router.post("/workers/{worker_id}/tasks/{task_id}/lease/renew") +async def renew_lease(worker_id: str, task_id: UUID, request: LeaseRenewRequest): + worker_id = _validate_worker_id(worker_id) + renewed = get_task_store().renew_remote_lease( + task_id, + worker_id, + request.lease_token, + lease_seconds=request.lease_seconds, + ) + if not renewed: + raise HTTPException( + status_code=409, + detail="Lease is missing, expired, or owned by another worker", + ) + return renewed + + +def _claimed_task_input_path( + task_id: UUID, + worker_id: str, + lease_token: str, +) -> Path | None: + """Resolve a lease-bound server upload without exposing arbitrary paths.""" + store = get_task_store() + if not store.get_remote_lease(task_id, worker_id, lease_token): + raise HTTPException( + status_code=409, + detail="Lease is missing, expired, or owned by another worker", + ) + task = store.get(task_id) + if not task: + raise HTTPException(status_code=404, detail="Task not found") + if ( + task.status != TaskStatus.PROCESSING + or task.requested_executor != "exe" + or task.assigned_executor != worker_id + ): + raise HTTPException(status_code=409, detail="Task is not assigned to this EXE lease") + + source = str(task.source or "").strip() + if source.startswith(("http://", "https://", "upload://")): + return None + + data_root = Path(get_runtime_settings().data_root).resolve() + staging_root = (data_root / "_staging").resolve() + source_path = Path(source) + try: + resolved = source_path.resolve(strict=True) + relative = resolved.relative_to(staging_root) + except (OSError, ValueError) as exc: + raise HTTPException( + status_code=403, + detail="Task input is outside the server staging directory", + ) from exc + if len(relative.parts) < 2 or source_path.is_symlink() or not resolved.is_file(): + raise HTTPException(status_code=403, detail="Task input is not a staged regular file") + try: + size = resolved.stat().st_size + except OSError as exc: + raise HTTPException(status_code=404, detail="Task input is unavailable") from exc + if size > MAX_UPLOAD_BYTES: + raise HTTPException(status_code=413, detail="Task input exceeds the 10 GB limit") + return resolved + + +@router.get("/workers/{worker_id}/tasks/{task_id}/input") +async def download_claimed_task_input( + worker_id: str, + task_id: UUID, + lease_token: str = Header( + ..., + alias="X-MPP-Lease-Token", + min_length=16, + max_length=512, + ), +): + """Download one server-staged input through its active EXE lease.""" + worker_id = _validate_worker_id(worker_id) + input_path = _claimed_task_input_path(task_id, worker_id, lease_token) + if input_path is None: + raise HTTPException(status_code=404, detail="Task has no staged input") + size = input_path.stat().st_size + return FileResponse( + input_path, + media_type="application/octet-stream", + filename=input_path.name, + headers={ + "Cache-Control": "private, no-store", + "X-MPP-Input-Size": str(size), + }, + ) + + +def _task_archive_destination(task_id: UUID) -> tuple[Path, Path]: + store = get_task_store() + task = store.get(task_id) + if not task: + raise HTTPException(status_code=404, detail="Task not found") + if task.requested_executor != "exe": + raise HTTPException(status_code=409, detail="Task is not assigned to an EXE executor") + + data_root = Path(get_runtime_settings().data_root).resolve() + result = task.result if isinstance(task.result, dict) else {} + destination_raw = result.get("output_dir") + destination = ( + Path(str(destination_raw)).resolve() + if destination_raw + else (data_root / f"remote-{task_id}").resolve() + ) + try: + destination.relative_to(data_root) + except ValueError as exc: + raise HTTPException( + status_code=409, + detail="Task output directory is outside data_root", + ) from exc + if destination == data_root: + raise HTTPException(status_code=409, detail="Task output directory is invalid") + return data_root, destination + + +def _prepare_uploaded_metadata(archive_root: Path, task_id: UUID) -> dict[str, Any]: + metadata_path = archive_root / "metadata.json" + try: + metadata = json.loads(metadata_path.read_text(encoding="utf-8")) + except (OSError, ValueError) as exc: + raise HTTPException(status_code=400, detail="Uploaded metadata.json is invalid") from exc + if not isinstance(metadata, dict): + raise HTTPException(status_code=400, detail="Uploaded metadata.json must be an object") + metadata["status"] = "completed" + metadata["task_id"] = str(task_id) + metadata_path.write_text( + json.dumps(metadata, ensure_ascii=False, indent=2), + encoding="utf-8", + ) + return metadata + + +def _parse_portable_result_json(raw: str) -> dict[str, Any]: + if len(raw.encode("utf-8")) > MAX_PORTABLE_RESULT_JSON_BYTES: + raise HTTPException(status_code=413, detail="result_json exceeds the 4 MB limit") + try: + payload = json.loads(raw or "{}") + except ValueError as exc: + raise HTTPException(status_code=400, detail="result_json is invalid JSON") from exc + if not isinstance(payload, dict): + raise HTTPException(status_code=400, detail="result_json must be an object") + return sanitize_portable_result(payload) + + +@router.post("/workers/{worker_id}/tasks/{task_id}/archive") +async def upload_completed_archive( + worker_id: str, + task_id: UUID, + lease_token: str = Form(...), + result_json: str = Form("{}"), + archive: UploadFile = File(...), +): + """Upload, validate, atomically publish, and finalize one EXE result ZIP.""" + worker_id = _validate_worker_id(worker_id) + store = get_task_store() + upload_lease = store.begin_remote_upload( + task_id, + worker_id, + lease_token, + lease_seconds=3600, + ) + if not upload_lease: + raise HTTPException( + status_code=409, + detail="Lease is unavailable or another upload is already in progress", + ) + if upload_lease["state"] == "completed": + task = store.get(task_id) + return {"task": task, "already_completed": True} + + staging_dir: Path | None = None + finalized = False + finalization_started = False + release_on_exit = True + + try: + portable_result = _parse_portable_result_json(result_json) + data_root, destination = _task_archive_destination(task_id) + staging_dir = data_root / "_remote_sync" / f"{task_id}-{uuid4().hex}" + zip_path = staging_dir / "archive.zip" + extraction_dir = staging_dir / "extracted" + staging_dir.mkdir(parents=True, exist_ok=False) + + upload_size, upload_sha256 = await stream_upload_to_path(archive, zip_path) + try: + extracted_root = await asyncio.to_thread(safe_extract_zip, zip_path, extraction_dir) + except (OSError, ValueError, zipfile.BadZipFile) as exc: + raise HTTPException( + status_code=400, + detail=f"Unsafe or invalid archive ZIP: {exc}", + ) from exc + + metadata = _prepare_uploaded_metadata(extracted_root, task_id) + if not store.begin_remote_finalization( + task_id, + worker_id, + lease_token, + lease_seconds=3600, + ): + raise HTTPException( + status_code=409, + detail="Task changed while the archive was uploading", + ) + finalization_started = True + + await asyncio.to_thread(publish_archive, extracted_root, destination, data_root) + await asyncio.to_thread(rewrite_archive_image_paths, destination) + portable_result = await asyncio.to_thread( + rewrite_result_image_paths, + portable_result, + destination, + ) + try: + metadata = json.loads( + (destination / "metadata.json").read_text(encoding="utf-8") + ) + except (OSError, ValueError) as exc: + raise HTTPException( + status_code=500, + detail="Published metadata.json could not be read", + ) from exc + files = await asyncio.to_thread(archive_file_manifest, destination) + previous = store.get(task_id) + previous_result = ( + dict(previous.result or {}) + if previous and isinstance(previous.result, dict) + else {} + ) + result = { + **previous_result, + **portable_result, + "metadata": metadata, + "output_dir": str(destination), + "archive": { + "output_dir": str(destination), + "files": files, + }, + "remote_sync": { + "worker_id": worker_id, + "archive_sha256": upload_sha256, + "archive_size": upload_size, + }, + } + task, already_completed = store.complete_remote_task( + task_id, + worker_id, + lease_token, + result, + ) + if not task: + raise HTTPException(status_code=409, detail="Lease expired before result finalization") + finalized = True + + await get_event_bus().publish( + TaskEvent( + task_id, + "completed", + { + "output_dir": str(destination), + "executor": worker_id, + "sync_revision": task.sync_revision, + }, + ) + ) + return {"task": task, "already_completed": already_completed} + except asyncio.CancelledError: + # A thread-backed publish may still be finishing after the request is + # cancelled. Keep its finalizing lease exclusive and let it expire + # before another worker can retry. + if finalization_started: + release_on_exit = False + raise + finally: + if not finalized and release_on_exit: + store.release_remote_upload(task_id, worker_id, lease_token) + if staging_dir is not None and (release_on_exit or finalized): + shutil.rmtree(staging_dir, ignore_errors=True) + + +@router.post("/workers/{worker_id}/tasks/{task_id}/fail") +async def fail_claimed_task(worker_id: str, task_id: UUID, request: WorkerFailureRequest): + worker_id = _validate_worker_id(worker_id) + task, already_failed = get_task_store().fail_remote_task( + task_id, + worker_id, + request.lease_token, + request.error, + ) + if not task: + raise HTTPException( + status_code=409, + detail="Lease is missing, expired, or owned by another worker", + ) + await get_event_bus().publish( + TaskEvent( + task_id, + "failed", + { + "error": request.error, + "executor": worker_id, + "sync_revision": task.sync_revision, + }, + ) + ) + return {"task": task, "already_failed": already_failed} + + +def _safe_download_name(task_title: str, task_id: UUID) -> str: + cleaned = re.sub(r'[<>:"/\\|?*\x00-\x1f]', "_", task_title).strip(" .") + return f"{(cleaned or str(task_id))[:100]}.zip" + + +@router.get("/sync/tasks/{task_id}/archive") +async def download_task_archive( + task_id: UUID, + include_media: bool = Query(False), +): + """Download a portable archive package; media is opt-in.""" + task = get_task_store().get(task_id) + if not task: + raise HTTPException(status_code=404, detail="Task not found") + if task.status != TaskStatus.COMPLETED: + raise HTTPException(status_code=409, detail="Task is not completed") + + data_root = Path(get_runtime_settings().data_root).resolve() + result = task.result if isinstance(task.result, dict) else {} + output_dir_raw = result.get("output_dir") + if not output_dir_raw: + raise HTTPException(status_code=404, detail="Task has no archive output") + output_dir = Path(str(output_dir_raw)).resolve() + try: + output_dir.relative_to(data_root) + except ValueError as exc: + raise HTTPException(status_code=403, detail="Task archive is outside data_root") from exc + if not output_dir.is_dir(): + raise HTTPException(status_code=404, detail="Task archive directory not found") + + download_dir = data_root / "_sync_downloads" / uuid4().hex + zip_path = download_dir / "archive.zip" + download_dir.mkdir(parents=True, exist_ok=False) + try: + await asyncio.to_thread( + build_archive_zip, + output_dir, + zip_path, + include_media=include_media, + ) + except (OSError, ValueError) as exc: + shutil.rmtree(download_dir, ignore_errors=True) + raise HTTPException(status_code=400, detail=f"Cannot package task archive: {exc}") from exc + + metadata = result.get("metadata") if isinstance(result.get("metadata"), dict) else {} + filename = _safe_download_name(str(metadata.get("title") or ""), task_id) + return FileResponse( + zip_path, + media_type="application/zip", + filename=filename, + background=BackgroundTask(shutil.rmtree, download_dir, ignore_errors=True), + ) diff --git a/backend/app/api/routes/tasks.py b/backend/app/api/routes/tasks.py index f74db62..387652c 100644 --- a/backend/app/api/routes/tasks.py +++ b/backend/app/api/routes/tasks.py @@ -11,6 +11,7 @@ import ipaddress import json import logging +import re from datetime import datetime from urllib.parse import urlparse from uuid import UUID @@ -21,15 +22,18 @@ from app.core.database import get_task_store from app.core.events import get_event_bus from app.core.pipeline import ( - PIPELINE_STEPS, PipelineStep, pipeline_steps_schema, _detect_source_type, _looks_like_local_path, + PIPELINE_STEPS, PipelineStep, pipeline_steps_schema, _looks_like_local_path, _clean_source_path, create_task_dir, write_metadata_json, ) from app.core.queue import get_task_queue +from app.core.settings import get_runtime_settings from app.core.source_resolver import resolve_source_flow -from app.models import Task, TaskBatchCreate, TaskCreate, TaskStatus +from app.models import Task, TaskBatchCreate, TaskCreate, TaskStatus, TaskType +from app.models.task import PREFERRED_WORKER_OPTION router = APIRouter(prefix="/tasks", tags=["tasks"]) logger = logging.getLogger(__name__) +_WORKER_ID_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$") def _validate_public_http_url(url: str) -> None: @@ -64,14 +68,62 @@ async def create_task(task_create: TaskCreate): from app.services.ingestion.ytdlp import normalize_bilibili_source_url source = normalize_bilibili_source_url(_clean_source_path(task_create.source)) + runtime = get_runtime_settings() + if runtime.remote_sync_enabled and task_create.task_type == TaskType.PIPELINE: + requested_executor = ( + task_create.requested_executor + if "requested_executor" in task_create.model_fields_set + else runtime.default_task_executor + ) + if ( + source.startswith(("http://", "https://", "upload://")) + or _looks_like_local_path(source) + ): + from app.services.remote_sync import ( + RemoteSyncConfigurationError, + RemoteSyncUnavailableError, + get_remote_sync_service, + ) + + forwarded = task_create.model_copy( + update={ + "source": source, + "requested_executor": requested_executor, + } + ) + try: + return await get_remote_sync_service().forward_task(forwarded) + except RemoteSyncConfigurationError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + except RemoteSyncUnavailableError as exc: + raise HTTPException(status_code=502, detail=str(exc)) from exc + + task_options = dict(task_create.options) + preferred_worker = str(task_options.get(PREFERRED_WORKER_OPTION) or "").strip() + if preferred_worker and not _WORKER_ID_PATTERN.fullmatch(preferred_worker): + raise HTTPException(status_code=400, detail="Invalid preferred EXE worker id") + if task_create.requested_executor != "exe": + preferred_worker = "" + task_options.pop(PREFERRED_WORKER_OPTION, None) task = Task( task_type=task_create.task_type, source=source, - options=task_create.options, + options=task_options, webhook_url=task_create.webhook_url, status=TaskStatus.QUEUED, + origin_client=task_create.origin_client, + requested_executor=task_create.requested_executor, + assigned_executor=( + "server" + if task_create.requested_executor == "server" + else preferred_worker or None + ), current_step=PipelineStep.DOWNLOAD, - message="等待处理...", + message=( + "等待处理..." + if task_create.requested_executor == "server" + else "等待 EXE 处理..." + ), steps=[s["id"] for s in PIPELINE_STEPS], completed_steps=[], ) @@ -134,20 +186,28 @@ async def create_tasks_batch(batch_create: TaskBatchCreate): tasks_created: list[Task] = [] for source in normalized_sources: - tasks_created.append(await create_task(TaskCreate( - task_type=batch_create.task_type, - source=source, - options=batch_create.options, - webhook_url=batch_create.webhook_url, - ))) + task_values = { + "task_type": batch_create.task_type, + "source": source, + "options": batch_create.options, + "webhook_url": batch_create.webhook_url, + "origin_client": batch_create.origin_client, + } + if "requested_executor" in batch_create.model_fields_set: + task_values["requested_executor"] = batch_create.requested_executor + tasks_created.append(await create_task(TaskCreate(**task_values))) return tasks_created @router.get("", response_model=list[Task]) -async def list_tasks(status: TaskStatus | None = None, limit: int = 50): +async def list_tasks( + status: TaskStatus | None = None, + limit: int = 50, + offset: int = 0, +): """List tasks with optional filtering.""" store = get_task_store() - return store.list(status=status, limit=limit) + return store.list(status=status, limit=limit, offset=offset) @router.get("/stats") diff --git a/backend/app/cli/main.py b/backend/app/cli/main.py index a10a835..7f2ad4d 100644 --- a/backend/app/cli/main.py +++ b/backend/app/cli/main.py @@ -621,7 +621,7 @@ def _do_attach(task_id: str, client=None, quiet: bool = False) -> None: # Rich progress display — uv-style: each completed step prints a line, # current step shows a live spinner+bar. - from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, TimeElapsedColumn, MofNCompleteColumn + from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn, TimeElapsedColumn from app.cli.display import STEP_LABELS src = current.get("source", task_id) @@ -748,7 +748,7 @@ def _print_final(task: dict, quiet: bool = False) -> None: def _print_detach_hint(task_id: str) -> None: from app.cli.display import console - console.print(f"\n[yellow]已脱离,任务仍在后台运行[/yellow]") + console.print("\n[yellow]已脱离,任务仍在后台运行[/yellow]") console.print(f" 查看进度: [bold]mpp attach {task_id[:8]}[/bold]") console.print(f" 查看结果: [bold]mpp show {task_id[:8]}[/bold]") @@ -868,7 +868,6 @@ def _tasks_watch(status_filter: str | None = None, limit: int = 20) -> None: from app.cli.display import console from rich.live import Live from rich.table import Table - from rich.text import Text from app.cli.display import styled_status, time_ago client = _require_daemon() @@ -946,10 +945,9 @@ def show( else: # Offline: read from SQLite try: - from app.core.database import get_task_store, init_db - from uuid import UUID + from app.core.database import init_db + init_db() - store = get_task_store() if task_ref.startswith("@"): tasks_offline = _list_tasks_any( @@ -1128,11 +1126,17 @@ def cancel( "local_llm_model_path", ], "security": [ - "api_token", + "api_token", "remote_api_token", "anthropic_api_key", "openai_api_key", "custom_api_key", "deepseek_api_key", "hf_token", "hf_proxy", "bilibili_sessdata", "bilibili_bili_jct", "bilibili_dede_user_id", ], + "sync": [ + "remote_sync_enabled", "remote_server_url", "remote_api_token", + "remote_worker_id", "remote_worker_name", "remote_sync_interval_sec", + "remote_sync_upload_results", "remote_sync_download_results", + "remote_sync_include_media", "default_task_executor", + ], "bilibili": [ "bilibili_sessdata", "bilibili_bili_jct", "bilibili_dede_user_id", ], @@ -1144,7 +1148,7 @@ def cancel( _SECRET_KEYS = { "anthropic_api_key", "openai_api_key", "custom_api_key", "deepseek_api_key", "siliconflow_api_key", - "hf_token", "hf_proxy", "api_token", "jina_reader_api_key", + "hf_token", "hf_proxy", "api_token", "remote_api_token", "jina_reader_api_key", "bilibili_sessdata", "bilibili_bili_jct", } @@ -1372,7 +1376,6 @@ def doctor(): ok = "[green]+" if _plain_mode else "[green]✓" err = "[red]x" if _plain_mode else "[red]✗" - warn = "[yellow]!" def check(label: str, passed: bool, detail: str = "") -> None: icon = ok if passed else err @@ -1385,7 +1388,7 @@ def check(label: str, passed: bool, detail: str = "") -> None: # Daemon client = _get_client() daemon_ok = client.ping() - check("Daemon", daemon_ok, "http://127.0.0.1:18000" if daemon_ok else "未运行 — mpp serve") + check("Daemon", daemon_ok, "http://localhost:18000" if daemon_ok else "未运行 — mpp serve") # ffmpeg ff = shutil.which("ffmpeg") diff --git a/backend/app/core/archive_sync.py b/backend/app/core/archive_sync.py new file mode 100644 index 0000000..06ef096 --- /dev/null +++ b/backend/app/core/archive_sync.py @@ -0,0 +1,612 @@ +"""Safe archive transfer helpers for coordinator/EXE synchronization.""" + +from __future__ import annotations + +import copy +import hashlib +import json +import os +import re +import shutil +import stat +import time +import zipfile +from pathlib import Path, PurePosixPath +from typing import Any +from uuid import uuid4 + +from fastapi import HTTPException, UploadFile + +MAX_UPLOAD_BYTES = 10 * 1024 * 1024 * 1024 +MAX_MEMBER_BYTES = 10 * 1024 * 1024 * 1024 +MAX_EXTRACTED_BYTES = 20 * 1024 * 1024 * 1024 +MAX_ARCHIVE_MEMBERS = 20_000 +MAX_COMPRESSION_RATIO = 200 +COMPRESSION_RATIO_ALLOWANCE = 10 * 1024 * 1024 +MAX_PORTABLE_RESULT_JSON_BYTES = 4 * 1024 * 1024 +MAX_STRUCTURED_JSON_BYTES = 16 * 1024 * 1024 +_BOUNDED_JSON_FILENAMES = {"metadata.json", "analysis.json"} +PORTABLE_RESULT_FIELDS = frozenset( + { + "metadata", + "image_descriptions", + "image_download_diagnostics", + "analysis", + "warnings", + "warning", + "content_subtype", + "subtitle_source", + "transcript_segments", + } +) + +MEDIA_EXTENSIONS = { + ".mp4", + ".mkv", + ".avi", + ".webm", + ".mov", + ".flv", + ".wmv", + ".mp3", + ".wav", + ".aac", + ".flac", + ".ogg", + ".m4a", + ".wma", + ".opus", +} +_SKIP_SUFFIXES = {".part", ".tmp", ".zip"} +_WINDOWS_RESERVED = re.compile(r"^(con|prn|aux|nul|com[0-9]|lpt[0-9])(?:\..*)?$", re.I) +_IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".avif"} +_SYNC_STAGING_ROOTS = ( + "_remote_sync", + "_sync_downloads", + "_remote_sync_client", +) +_SYNC_BACKUP_PATTERN = re.compile( + r"^\.(?P[^/\\]+)\.sync-backup-[0-9a-f]{32}$", + re.IGNORECASE, +) + + +def sweep_stale_sync_storage( + data_root: Path, + *, + max_age_seconds: float = 24 * 60 * 60, +) -> dict[str, int]: + """Recover interrupted archive swaps and remove bounded stale sync staging.""" + root = data_root.resolve() + now = time.time() + removed = 0 + restored = 0 + + for root_name in _SYNC_STAGING_ROOTS: + staging_root = root / root_name + if not staging_root.is_dir() or staging_root.is_symlink(): + continue + for child in list(staging_root.iterdir()): + try: + resolved = child.resolve() + resolved.relative_to(staging_root.resolve()) + if child.is_symlink(): + continue + age = max(0.0, now - child.stat().st_mtime) + if age < max_age_seconds: + continue + if child.is_dir(): + shutil.rmtree(resolved) + else: + resolved.unlink() + removed += 1 + except (OSError, ValueError): + continue + + if root.is_dir(): + for backup in list(root.iterdir()): + match = _SYNC_BACKUP_PATTERN.fullmatch(backup.name) + if not match or not backup.is_dir() or backup.is_symlink(): + continue + target = root / match.group("target") + try: + backup.resolve().relative_to(root) + target.resolve().relative_to(root) + if not target.exists(): + os.replace(backup, target) + restored += 1 + continue + age = max(0.0, now - backup.stat().st_mtime) + if age >= max_age_seconds: + shutil.rmtree(backup) + removed += 1 + except (OSError, ValueError): + continue + + return {"removed": removed, "restored": restored} + + +def _archive_image_index( + archive_dir: Path, +) -> tuple[dict[str, Path], dict[str, Path | None]]: + images_dir = archive_dir / "images" + by_relative: dict[str, Path] = {} + by_basename: dict[str, Path | None] = {} + if not images_dir.is_dir(): + return by_relative, by_basename + + for image in sorted(images_dir.rglob("*")): + if not image.is_file() or image.suffix.lower() not in _IMAGE_EXTENSIONS: + continue + relative = image.relative_to(images_dir).as_posix().casefold() + by_relative[relative] = image + basename = image.name.casefold() + if basename in by_basename: + by_basename[basename] = None + else: + by_basename[basename] = image + return by_relative, by_basename + + +def _match_archive_image( + raw_path: Any, + by_relative: dict[str, Path], + by_basename: dict[str, Path | None], +) -> Path | None: + if not isinstance(raw_path, str) or not raw_path.strip(): + return None + normalized = raw_path.strip().replace("\\", "/") + parts = PurePosixPath(normalized).parts + image_positions = [ + index + for index, part in enumerate(parts) + if part.casefold() == "images" + ] + if image_positions: + relative_parts = parts[image_positions[-1] + 1 :] + if ( + relative_parts + and not any(part in {"", ".", ".."} for part in relative_parts) + ): + match = by_relative.get( + PurePosixPath(*relative_parts).as_posix().casefold() + ) + if match is not None: + return match + basename = PurePosixPath(normalized).name.casefold() + return by_basename.get(basename) + + +def _render_archive_image_path( + image: Path, + archive_dir: Path, + *, + relative: bool, +) -> str: + if relative: + return image.relative_to(archive_dir).as_posix() + return str(image.resolve()) + + +def _rewrite_metadata_image_paths( + payload: dict[str, Any], + archive_dir: Path, + by_relative: dict[str, Path], + by_basename: dict[str, Path | None], + *, + relative: bool, +) -> None: + extra = payload.get("extra") + if not isinstance(extra, dict): + return + + downloaded = extra.get("downloaded_image_paths") + if isinstance(downloaded, list): + rewritten: list[str] = [] + for raw_path in downloaded: + image = _match_archive_image(raw_path, by_relative, by_basename) + if image is not None: + rewritten.append( + _render_archive_image_path( + image, + archive_dir, + relative=relative, + ) + ) + extra["downloaded_image_paths"] = rewritten + + thumbnail_source = extra.get("thumbnail_source") + image = _match_archive_image(thumbnail_source, by_relative, by_basename) + if image is not None: + extra["thumbnail_source"] = _render_archive_image_path( + image, + archive_dir, + relative=relative, + ) + + +def _rewrite_nested_image_descriptions( + value: Any, + archive_dir: Path, + by_relative: dict[str, Path], + by_basename: dict[str, Path | None], + *, + relative: bool, +) -> None: + if isinstance(value, list): + for item in value: + _rewrite_nested_image_descriptions( + item, + archive_dir, + by_relative, + by_basename, + relative=relative, + ) + return + if not isinstance(value, dict): + return + + descriptions = value.get("image_descriptions") + if isinstance(descriptions, list): + for description in descriptions: + if not isinstance(description, dict): + continue + image = _match_archive_image( + description.get("image_path"), + by_relative, + by_basename, + ) + if image is not None: + description["image_path"] = _render_archive_image_path( + image, + archive_dir, + relative=relative, + ) + + for child in value.values(): + _rewrite_nested_image_descriptions( + child, + archive_dir, + by_relative, + by_basename, + relative=relative, + ) + + +def rewrite_result_image_paths( + result: dict[str, Any], + archive_dir: Path, + *, + relative: bool = False, +) -> dict[str, Any]: + """Rewrite portable image references against one published archive.""" + rewritten = copy.deepcopy(result) + by_relative, by_basename = _archive_image_index(archive_dir) + if not by_relative: + return rewritten + + metadata = rewritten.get("metadata") + if isinstance(metadata, dict): + _rewrite_metadata_image_paths( + metadata, + archive_dir, + by_relative, + by_basename, + relative=relative, + ) + _rewrite_nested_image_descriptions( + rewritten, + archive_dir, + by_relative, + by_basename, + relative=relative, + ) + return rewritten + + +def sanitize_portable_result(result: Any) -> dict[str, Any]: + """Keep result metadata needed by readers while dropping local filesystem state.""" + if not isinstance(result, dict): + return {} + return { + key: copy.deepcopy(result[key]) + for key in PORTABLE_RESULT_FIELDS + if key in result + } + + +def rewrite_archive_image_paths(archive_dir: Path) -> None: + """Localize image references embedded in portable archive JSON files.""" + by_relative, by_basename = _archive_image_index(archive_dir) + if not by_relative: + return + + for filename in ("metadata.json", "analysis.json"): + path = archive_dir / filename + if not path.is_file(): + continue + try: + if path.stat().st_size > MAX_STRUCTURED_JSON_BYTES: + continue + payload = json.loads(path.read_text(encoding="utf-8")) + except (OSError, ValueError): + continue + if not isinstance(payload, dict): + continue + if filename == "metadata.json": + _rewrite_metadata_image_paths( + payload, + archive_dir, + by_relative, + by_basename, + relative=False, + ) + _rewrite_nested_image_descriptions( + payload, + archive_dir, + by_relative, + by_basename, + relative=False, + ) + path.write_text( + json.dumps(payload, ensure_ascii=False, indent=2), + encoding="utf-8", + ) + + +async def stream_upload_to_path( + upload: UploadFile, + destination: Path, + *, + max_bytes: int = MAX_UPLOAD_BYTES, +) -> tuple[int, str]: + """Stream an upload to disk while enforcing size and calculating SHA-256.""" + destination.parent.mkdir(parents=True, exist_ok=True) + written = 0 + digest = hashlib.sha256() + try: + with destination.open("wb") as output: + while True: + chunk = await upload.read(8 * 1024 * 1024) + if not chunk: + break + written += len(chunk) + if written > max_bytes: + raise HTTPException( + status_code=413, + detail="Archive upload exceeds the 10 GB limit", + ) + digest.update(chunk) + output.write(chunk) + except Exception: + destination.unlink(missing_ok=True) + raise + finally: + await upload.close() + return written, digest.hexdigest() + + +def _safe_member_path(raw_name: str) -> PurePosixPath: + normalized = raw_name.replace("\\", "/") + if not normalized or normalized.startswith("/") or re.match(r"^[A-Za-z]:", normalized): + raise ValueError(f"unsafe archive member path: {raw_name!r}") + if "\x00" in normalized or any(ord(char) < 32 for char in normalized): + raise ValueError(f"unsafe archive member path: {raw_name!r}") + + path = PurePosixPath(normalized) + if not path.parts or any(part in {"", ".", ".."} for part in path.parts): + raise ValueError(f"unsafe archive member path: {raw_name!r}") + for part in path.parts: + if ":" in part or part.endswith((" ", ".")) or _WINDOWS_RESERVED.match(part): + raise ValueError(f"unsafe archive member path: {raw_name!r}") + return path + + +def _is_symlink(info: zipfile.ZipInfo) -> bool: + unix_mode = (info.external_attr >> 16) & 0xFFFF + return stat.S_IFMT(unix_mode) == stat.S_IFLNK + + +def safe_extract_zip(zip_path: Path, extraction_dir: Path) -> Path: + """Validate and extract an EXE-produced archive. + + The archive may contain files at its root or one top-level directory. + ``metadata.json`` is required so the server can expose the result through + the existing archive APIs. + """ + extraction_dir.mkdir(parents=True, exist_ok=False) + extracted_total = 0 + seen: set[str] = set() + + try: + with zipfile.ZipFile(zip_path, "r") as archive: + infos = archive.infolist() + if not infos or len(infos) > MAX_ARCHIVE_MEMBERS: + raise ValueError("archive has an invalid number of members") + + validated: list[tuple[zipfile.ZipInfo, PurePosixPath]] = [] + for info in infos: + member_path = _safe_member_path(info.filename) + collision_key = member_path.as_posix().casefold() + if collision_key in seen: + raise ValueError(f"duplicate archive member: {info.filename!r}") + seen.add(collision_key) + + if info.flag_bits & 0x1: + raise ValueError("encrypted ZIP members are unsupported") + if _is_symlink(info): + raise ValueError(f"symbolic links are unsupported: {info.filename!r}") + if info.file_size < 0 or info.file_size > MAX_MEMBER_BYTES: + raise ValueError(f"archive member is too large: {info.filename!r}") + if ( + member_path.name.casefold() in _BOUNDED_JSON_FILENAMES + and info.file_size > MAX_STRUCTURED_JSON_BYTES + ): + raise ValueError( + f"structured JSON member is too large: {info.filename!r}" + ) + extracted_total += info.file_size + if extracted_total > MAX_EXTRACTED_BYTES: + raise ValueError("archive expands beyond the 20 GB limit") + if ( + not info.is_dir() + and info.file_size > COMPRESSION_RATIO_ALLOWANCE + and info.file_size + > info.compress_size * MAX_COMPRESSION_RATIO + COMPRESSION_RATIO_ALLOWANCE + ): + raise ValueError(f"suspicious ZIP compression ratio: {info.filename!r}") + validated.append((info, member_path)) + + root = extraction_dir.resolve() + actual_total = 0 + for info, member_path in validated: + target = (extraction_dir / Path(*member_path.parts)).resolve() + target.relative_to(root) + if info.is_dir(): + target.mkdir(parents=True, exist_ok=True) + continue + target.parent.mkdir(parents=True, exist_ok=True) + member_written = 0 + with archive.open(info, "r") as source, target.open("xb") as output: + while True: + chunk = source.read(1024 * 1024) + if not chunk: + break + member_written += len(chunk) + actual_total += len(chunk) + if member_written > info.file_size or actual_total > MAX_EXTRACTED_BYTES: + raise ValueError("archive expanded beyond its declared size") + output.write(chunk) + if member_written != info.file_size: + raise ValueError(f"archive member size mismatch: {info.filename!r}") + + if (extraction_dir / "metadata.json").is_file(): + return extraction_dir + children = [child for child in extraction_dir.iterdir()] + if ( + len(children) == 1 + and children[0].is_dir() + and (children[0] / "metadata.json").is_file() + ): + return children[0] + raise ValueError("archive must contain metadata.json at its root") + except Exception: + shutil.rmtree(extraction_dir, ignore_errors=True) + raise + + +def publish_archive(extracted_root: Path, destination: Path, data_root: Path) -> None: + """Atomically replace a task's server-side archive directory.""" + root = data_root.resolve() + target = destination.resolve() + target.relative_to(root) + if target == root: + raise ValueError("archive destination cannot be data_root") + + backup = target.parent / f".{target.name}.sync-backup-{uuid4().hex}" + moved_existing = False + try: + if target.exists(): + os.replace(target, backup) + moved_existing = True + os.replace(extracted_root, target) + except Exception: + if target.exists() and not moved_existing: + shutil.rmtree(target, ignore_errors=True) + if moved_existing and backup.exists() and not target.exists(): + os.replace(backup, target) + raise + else: + if backup.exists(): + shutil.rmtree(backup, ignore_errors=True) + + +def archive_file_manifest(archive_dir: Path) -> dict[str, str]: + """Return the existing archive-result ``files`` mapping with safe relative keys.""" + files: dict[str, str] = {} + for path in sorted(archive_dir.rglob("*")): + if not path.is_file() or path.is_symlink(): + continue + relative = path.relative_to(archive_dir).as_posix() + files[relative] = str(path) + return files + + +def build_archive_zip( + archive_dir: Path, + destination: Path, + *, + include_media: bool = False, +) -> dict[str, Any]: + """Create a portable result ZIP and embed a checksum manifest.""" + archive_root = archive_dir.resolve() + candidates: list[tuple[Path, str]] = [] + for path in sorted(archive_root.rglob("*")): + if path.is_symlink(): + raise ValueError(f"archive contains a symbolic link: {path}") + if not path.is_file(): + continue + relative = path.relative_to(archive_root).as_posix() + safe_relative = _safe_member_path(relative).as_posix() + if safe_relative == "_sync_manifest.json": + continue + if path.suffix.lower() in _SKIP_SUFFIXES: + continue + if not include_media and path.suffix.lower() in MEDIA_EXTENSIONS: + continue + candidates.append((path, safe_relative)) + if len(candidates) + 1 > MAX_ARCHIVE_MEMBERS: + raise ValueError("archive has too many files") + + destination.parent.mkdir(parents=True, exist_ok=True) + manifest_files: list[dict[str, Any]] = [] + try: + with zipfile.ZipFile( + destination, + "w", + compression=zipfile.ZIP_DEFLATED, + compresslevel=6, + allowZip64=True, + ) as archive: + for source_path, relative in candidates: + digest = hashlib.sha256() + size = 0 + with ( + source_path.open("rb") as source, + archive.open(relative, "w", force_zip64=True) as output, + ): + while True: + chunk = source.read(1024 * 1024) + if not chunk: + break + size += len(chunk) + if size > MAX_MEMBER_BYTES: + raise ValueError(f"archive member is too large: {relative}") + digest.update(chunk) + output.write(chunk) + manifest_files.append( + { + "path": relative, + "size": size, + "sha256": digest.hexdigest(), + } + ) + + manifest = { + "version": 1, + "include_media": include_media, + "files": manifest_files, + } + archive.writestr( + "_sync_manifest.json", + json.dumps(manifest, ensure_ascii=False, indent=2).encode("utf-8"), + ) + except Exception: + destination.unlink(missing_ok=True) + raise + + return { + "include_media": include_media, + "file_count": len(manifest_files), + "files": manifest_files, + } diff --git a/backend/app/core/database.py b/backend/app/core/database.py index ca2bf4f..5d95b04 100644 --- a/backend/app/core/database.py +++ b/backend/app/core/database.py @@ -6,19 +6,22 @@ from __future__ import annotations +import hashlib import json import logging +import secrets import sqlite3 import threading -from datetime import datetime +from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Any from uuid import UUID, uuid4 from app.core.logging_setup import log_event -from app.models.task import Task, TaskStatus, TaskType +from app.models.task import PREFERRED_WORKER_OPTION, Task, TaskStatus, TaskType logger = logging.getLogger(__name__) +_MAX_REMOTE_LEASE_ATTEMPTS = 3 # DB path - resolved at init time from settings _db_path: Path | None = None @@ -43,7 +46,11 @@ current_step TEXT, steps TEXT NOT NULL DEFAULT '[]', completed_steps TEXT NOT NULL DEFAULT '[]', - flow TEXT + flow TEXT, + origin_client TEXT NOT NULL DEFAULT 'legacy', + requested_executor TEXT NOT NULL DEFAULT 'server', + assigned_executor TEXT, + sync_revision INTEGER NOT NULL DEFAULT 0 ); CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status); @@ -76,6 +83,33 @@ ); CREATE INDEX IF NOT EXISTS idx_task_events_task_id_id ON task_events(task_id, id); + +CREATE TABLE IF NOT EXISTS workers ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + capabilities TEXT NOT NULL DEFAULT '{}', + status TEXT NOT NULL DEFAULT 'online', + registered_at TEXT NOT NULL, + last_seen_at TEXT NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_workers_last_seen ON workers(last_seen_at DESC); + +CREATE TABLE IF NOT EXISTS task_leases ( + task_id TEXT PRIMARY KEY, + worker_id TEXT NOT NULL, + token_hash TEXT NOT NULL, + state TEXT NOT NULL DEFAULT 'active', + attempt INTEGER NOT NULL DEFAULT 1, + claimed_at TEXT NOT NULL, + lease_expires_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE, + FOREIGN KEY (worker_id) REFERENCES workers(id) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS idx_task_leases_worker_state + ON task_leases(worker_id, state, lease_expires_at); """ # Columns added after initial schema — applied idempotently via ALTER TABLE @@ -84,7 +118,16 @@ "ALTER TABLE tasks ADD COLUMN uploader_id TEXT", "ALTER TABLE tasks ADD COLUMN content_subtype TEXT", "ALTER TABLE tasks ADD COLUMN flow TEXT", + "ALTER TABLE tasks ADD COLUMN origin_client TEXT NOT NULL DEFAULT 'legacy'", + "ALTER TABLE tasks ADD COLUMN requested_executor TEXT NOT NULL DEFAULT 'server'", + "ALTER TABLE tasks ADD COLUMN assigned_executor TEXT", + "ALTER TABLE tasks ADD COLUMN sync_revision INTEGER NOT NULL DEFAULT 0", "CREATE INDEX IF NOT EXISTS idx_tasks_platform ON tasks(platform)", + ( + "CREATE INDEX IF NOT EXISTS idx_tasks_executor_status_created " + "ON tasks(requested_executor, status, created_at)" + ), + "CREATE INDEX IF NOT EXISTS idx_tasks_assigned_executor ON tasks(assigned_executor)", ] @@ -162,6 +205,10 @@ def _task_to_row(task: Task) -> dict: "platform": platform, "uploader_id": uploader_id, "content_subtype": content_subtype, + "origin_client": task.origin_client, + "requested_executor": task.requested_executor, + "assigned_executor": task.assigned_executor, + "sync_revision": task.sync_revision, } @@ -189,21 +236,65 @@ def _row_to_task(row: sqlite3.Row) -> Task: platform=row["platform"] if "platform" in keys else None, uploader_id=row["uploader_id"] if "uploader_id" in keys else None, content_subtype=row["content_subtype"] if "content_subtype" in keys else None, + origin_client=( + row["origin_client"] + if "origin_client" in keys and row["origin_client"] + else "legacy" + ), + requested_executor=( + row["requested_executor"] + if "requested_executor" in keys and row["requested_executor"] in {"server", "exe"} + else "server" + ), + assigned_executor=row["assigned_executor"] if "assigned_executor" in keys else None, + sync_revision=int(row["sync_revision"] or 0) if "sync_revision" in keys else 0, ) +def _utc_now() -> datetime: + return datetime.now(timezone.utc) + + +def _lease_token_hash(token: str) -> str: + return hashlib.sha256(token.encode("utf-8")).hexdigest() + + +def _parse_timestamp(value: str) -> datetime: + parsed = datetime.fromisoformat(value) + return parsed if parsed.tzinfo is not None else parsed.replace(tzinfo=timezone.utc) + + class TaskStore: """SQLite-backed task persistence.""" def save(self, task: Task) -> None: - """Insert or replace a task.""" + """Insert a task or update it while preserving related rows.""" + self._upsert(task, exact_sync_revision=False) + + def save_remote_mirror(self, task: Task) -> None: + """Upsert a coordinator mirror using its canonical sync revision.""" + self._upsert(task, exact_sync_revision=True) + + def _upsert(self, task: Task, *, exact_sync_revision: bool) -> None: conn = _get_conn() row = _task_to_row(task) cols = ", ".join(row.keys()) placeholders = ", ".join(f":{k}" for k in row.keys()) + updates = [ + f"{key} = excluded.{key}" + for key in row.keys() + if key not in {"id", "sync_revision"} + ] + if exact_sync_revision: + updates.append("sync_revision = excluded.sync_revision") + else: + updates.append("sync_revision = tasks.sync_revision + 1") with _db_lock: conn.execute( - f"INSERT OR REPLACE INTO tasks ({cols}) VALUES ({placeholders})", + f""" + INSERT INTO tasks ({cols}) VALUES ({placeholders}) + ON CONFLICT(id) DO UPDATE SET {", ".join(updates)} + """, row, ) conn.commit() @@ -215,6 +306,77 @@ def get(self, task_id: UUID) -> Task | None: row = cur.fetchone() return _row_to_task(row) if row else None + def find_by_output_dir(self, output_dir: Path | str) -> Task | None: + """Resolve a task whose result points at one local archive directory.""" + target = str(Path(output_dir).resolve()) + for task in self.list(limit=10_000): + result = task.result if isinstance(task.result, dict) else {} + archive = result.get("archive") + candidates = [ + result.get("output_dir"), + archive.get("output_dir") if isinstance(archive, dict) else None, + ] + for candidate in candidates: + if not candidate: + continue + try: + if str(Path(str(candidate)).resolve()) == target: + return task + except OSError: + continue + return None + + def touch_sync_revision( + self, + task_id: UUID, + *, + result_updates: dict[str, Any] | None = None, + ) -> Task | None: + """Atomically bump a task revision after an archive-side mutation.""" + conn = _get_conn() + now = datetime.now().isoformat() + with _db_lock: + try: + conn.execute("BEGIN IMMEDIATE") + row = conn.execute( + "SELECT result FROM tasks WHERE id = ?", + (str(task_id),), + ).fetchone() + if not row: + conn.rollback() + return None + try: + result = json.loads(row["result"]) if row["result"] else {} + except (TypeError, ValueError): + result = {} + if not isinstance(result, dict): + result = {} + if result_updates: + result.update(result_updates) + conn.execute( + """ + UPDATE tasks + SET result = ?, + sync_revision = sync_revision + 1, + updated_at = ? + WHERE id = ? + """, + ( + json.dumps(result, ensure_ascii=False), + now, + str(task_id), + ), + ) + updated = conn.execute( + "SELECT * FROM tasks WHERE id = ?", + (str(task_id),), + ).fetchone() + conn.commit() + except Exception: + conn.rollback() + raise + return _row_to_task(updated) if updated else None + def list( self, status: str | None = None, @@ -245,6 +407,871 @@ def list_by_statuses(self, statuses: list[str]) -> list[Task]: ) return [_row_to_task(r) for r in cur.fetchall()] + def register_worker( + self, + *, + worker_id: str | None, + name: str, + capabilities: dict[str, Any] | None = None, + ) -> dict[str, Any]: + """Register or refresh one EXE worker. + + A caller may persist ``worker_id`` locally and pass it again on the next + startup. Supplying no id creates a stable opaque id for the caller to + save after registration. + """ + conn = _get_conn() + resolved_id = worker_id or f"exe-{uuid4().hex}" + now = _utc_now().isoformat() + payload = json.dumps(capabilities or {}, ensure_ascii=False) + with _db_lock: + conn.execute( + """ + INSERT INTO workers + (id, name, capabilities, status, registered_at, last_seen_at) + VALUES (?, ?, ?, 'online', ?, ?) + ON CONFLICT(id) DO UPDATE SET + name = excluded.name, + capabilities = excluded.capabilities, + status = 'online', + last_seen_at = excluded.last_seen_at + """, + (resolved_id, name, payload, now, now), + ) + conn.commit() + return self.get_worker(resolved_id) or {} + + def get_worker(self, worker_id: str) -> dict[str, Any] | None: + conn = _get_conn() + row = conn.execute( + """ + SELECT id, name, capabilities, status, registered_at, last_seen_at + FROM workers + WHERE id = ? + """, + (worker_id,), + ).fetchone() + if not row: + return None + record = dict(row) + try: + record["capabilities"] = json.loads(record["capabilities"] or "{}") + except json.JSONDecodeError: + record["capabilities"] = {} + return record + + def list_workers(self) -> list[dict[str, Any]]: + conn = _get_conn() + rows = conn.execute( + """ + SELECT id, name, capabilities, status, registered_at, last_seen_at + FROM workers + ORDER BY last_seen_at DESC + """ + ).fetchall() + records: list[dict[str, Any]] = [] + for row in rows: + record = dict(row) + try: + record["capabilities"] = json.loads(record["capabilities"] or "{}") + except json.JSONDecodeError: + record["capabilities"] = {} + records.append(record) + return records + + def heartbeat_worker( + self, + worker_id: str, + *, + capabilities: dict[str, Any] | None = None, + ) -> dict[str, Any] | None: + conn = _get_conn() + now = _utc_now().isoformat() + with _db_lock: + if capabilities is None: + cur = conn.execute( + """ + UPDATE workers + SET status = 'online', last_seen_at = ? + WHERE id = ? + """, + (now, worker_id), + ) + else: + cur = conn.execute( + """ + UPDATE workers + SET status = 'online', last_seen_at = ?, capabilities = ? + WHERE id = ? + """, + (now, json.dumps(capabilities, ensure_ascii=False), worker_id), + ) + conn.commit() + if cur.rowcount == 0: + return None + return self.get_worker(worker_id) + + def _expire_remote_leases_locked(self, conn: sqlite3.Connection, now: datetime) -> int: + rows = conn.execute( + """ + SELECT l.task_id, l.attempt, t.options + FROM task_leases AS l + JOIN tasks AS t ON t.id = l.task_id + WHERE l.state IN ('active', 'uploading', 'finalizing') + AND l.lease_expires_at <= ? + """, + (now.isoformat(),), + ).fetchall() + if not rows: + return 0 + task_ids = [str(row["task_id"]) for row in rows] + placeholders = ", ".join("?" for _ in task_ids) + conn.execute( + f""" + UPDATE task_leases + SET state = 'expired', updated_at = ? + WHERE task_id IN ({placeholders}) + AND state IN ('active', 'uploading', 'finalizing') + """, + [now.isoformat(), *task_ids], + ) + for row in rows: + if int(row["attempt"] or 0) >= _MAX_REMOTE_LEASE_ATTEMPTS: + conn.execute( + """ + UPDATE task_leases + SET state = 'failed', updated_at = ? + WHERE task_id = ? AND state = 'expired' + """, + (now.isoformat(), str(row["task_id"])), + ) + conn.execute( + """ + UPDATE tasks + SET status = 'failed', + message = 'EXE 任务多次中断,已停止自动重试', + error = 'EXE worker lease expired repeatedly', + completed_at = ?, + updated_at = ?, + sync_revision = sync_revision + 1 + WHERE id = ? + AND requested_executor = 'exe' + AND status = 'processing' + """, + ( + now.isoformat(), + now.isoformat(), + str(row["task_id"]), + ), + ) + continue + try: + options = json.loads(str(row["options"] or "{}")) + except (TypeError, ValueError): + options = {} + preferred_worker = ( + str(options.get(PREFERRED_WORKER_OPTION) or "").strip() + if isinstance(options, dict) + else "" + ) + conn.execute( + """ + UPDATE tasks + SET status = 'queued', + assigned_executor = ?, + message = '等待 EXE 处理...', + updated_at = ?, + sync_revision = sync_revision + 1 + WHERE id = ? + AND requested_executor = 'exe' + AND status = 'processing' + """, + (preferred_worker or None, now.isoformat(), str(row["task_id"])), + ) + return len(task_ids) + + def claim_remote_task( + self, + worker_id: str, + *, + lease_seconds: int = 900, + ) -> dict[str, Any] | None: + """Atomically lease the oldest queued ``requested_executor=exe`` task.""" + conn = _get_conn() + now = _utc_now() + expires_at = now + timedelta(seconds=lease_seconds) + token = secrets.token_urlsafe(32) + token_hash = _lease_token_hash(token) + + with _db_lock: + try: + conn.execute("BEGIN IMMEDIATE") + worker = conn.execute( + "SELECT id FROM workers WHERE id = ?", + (worker_id,), + ).fetchone() + if not worker: + conn.rollback() + return None + + self._expire_remote_leases_locked(conn, now) + row = conn.execute( + """ + SELECT t.* + FROM tasks AS t + LEFT JOIN task_leases AS l ON l.task_id = t.id + WHERE t.requested_executor = 'exe' + AND t.status = 'queued' + AND (t.assigned_executor IS NULL OR t.assigned_executor = ?) + AND ( + l.task_id IS NULL + OR l.state NOT IN ('active', 'uploading', 'finalizing') + ) + ORDER BY t.created_at ASC + LIMIT 1 + """, + (worker_id,), + ).fetchone() + if not row: + conn.commit() + return None + + task_id = str(row["id"]) + previous = conn.execute( + "SELECT attempt FROM task_leases WHERE task_id = ?", + (task_id,), + ).fetchone() + attempt = int(previous["attempt"] or 0) + 1 if previous else 1 + updated = conn.execute( + """ + UPDATE tasks + SET status = 'processing', + assigned_executor = ?, + message = 'EXE 正在处理...', + updated_at = ?, + sync_revision = sync_revision + 1 + WHERE id = ? + AND status = 'queued' + AND requested_executor = 'exe' + AND (assigned_executor IS NULL OR assigned_executor = ?) + """, + (worker_id, now.isoformat(), task_id, worker_id), + ) + if updated.rowcount != 1: + conn.rollback() + return None + conn.execute( + """ + INSERT INTO task_leases + (task_id, worker_id, token_hash, state, attempt, + claimed_at, lease_expires_at, updated_at) + VALUES (?, ?, ?, 'active', ?, ?, ?, ?) + ON CONFLICT(task_id) DO UPDATE SET + worker_id = excluded.worker_id, + token_hash = excluded.token_hash, + state = 'active', + attempt = excluded.attempt, + claimed_at = excluded.claimed_at, + lease_expires_at = excluded.lease_expires_at, + updated_at = excluded.updated_at + """, + ( + task_id, + worker_id, + token_hash, + attempt, + now.isoformat(), + expires_at.isoformat(), + now.isoformat(), + ), + ) + conn.execute( + """ + UPDATE workers + SET status = 'online', last_seen_at = ? + WHERE id = ? + """, + (now.isoformat(), worker_id), + ) + claimed_row = conn.execute( + "SELECT * FROM tasks WHERE id = ?", + (task_id,), + ).fetchone() + conn.commit() + except Exception: + conn.rollback() + raise + + if not claimed_row: + return None + return { + "task": _row_to_task(claimed_row), + "lease_token": token, + "lease_expires_at": expires_at.isoformat(), + "attempt": attempt, + } + + def get_remote_lease( + self, + task_id: UUID | str, + worker_id: str, + lease_token: str, + *, + allow_completed: bool = False, + ) -> dict[str, Any] | None: + """Validate lease ownership and return its state.""" + conn = _get_conn() + row = conn.execute( + """ + SELECT task_id, worker_id, token_hash, state, attempt, + claimed_at, lease_expires_at, updated_at + FROM task_leases + WHERE task_id = ? AND worker_id = ? + """, + (str(task_id), worker_id), + ).fetchone() + if not row or not secrets.compare_digest( + str(row["token_hash"]), + _lease_token_hash(lease_token), + ): + return None + record = dict(row) + if record["state"] == "completed" and allow_completed: + return record + if record["state"] != "active": + return None + if _parse_timestamp(str(record["lease_expires_at"])) <= _utc_now(): + return None + return record + + def renew_remote_lease( + self, + task_id: UUID | str, + worker_id: str, + lease_token: str, + *, + lease_seconds: int = 900, + ) -> dict[str, Any] | None: + conn = _get_conn() + now = _utc_now() + expires_at = now + timedelta(seconds=lease_seconds) + with _db_lock: + try: + conn.execute("BEGIN IMMEDIATE") + row = conn.execute( + """ + SELECT l.token_hash, l.state, l.lease_expires_at, + t.status AS task_status, + t.requested_executor, + t.assigned_executor + FROM task_leases AS l + JOIN tasks AS t ON t.id = l.task_id + WHERE l.task_id = ? AND l.worker_id = ? + """, + (str(task_id), worker_id), + ).fetchone() + valid = ( + row + and row["state"] in {"active", "uploading", "finalizing"} + and secrets.compare_digest( + str(row["token_hash"]), + _lease_token_hash(lease_token), + ) + and _parse_timestamp(str(row["lease_expires_at"])) > now + and row["task_status"] == TaskStatus.PROCESSING.value + and row["requested_executor"] == "exe" + and row["assigned_executor"] == worker_id + ) + if not valid: + conn.rollback() + return None + conn.execute( + """ + UPDATE task_leases + SET lease_expires_at = ?, updated_at = ? + WHERE task_id = ? + AND worker_id = ? + AND state IN ('active', 'uploading', 'finalizing') + """, + (expires_at.isoformat(), now.isoformat(), str(task_id), worker_id), + ) + conn.execute( + """ + UPDATE workers + SET status = 'online', last_seen_at = ? + WHERE id = ? + """, + (now.isoformat(), worker_id), + ) + conn.commit() + except Exception: + conn.rollback() + raise + return { + "task_id": str(task_id), + "worker_id": worker_id, + "lease_expires_at": expires_at.isoformat(), + } + + def begin_remote_upload( + self, + task_id: UUID | str, + worker_id: str, + lease_token: str, + *, + lease_seconds: int = 3600, + ) -> dict[str, Any] | None: + """Exclusively move one active lease into the archive-upload phase.""" + conn = _get_conn() + now = _utc_now() + expires_at = now + timedelta(seconds=lease_seconds) + with _db_lock: + try: + conn.execute("BEGIN IMMEDIATE") + self._expire_remote_leases_locked(conn, now) + row = conn.execute( + """ + SELECT l.task_id, l.worker_id, l.token_hash, l.state, l.attempt, + l.claimed_at, l.lease_expires_at, l.updated_at, + t.status AS task_status, + t.requested_executor, + t.assigned_executor + FROM task_leases AS l + JOIN tasks AS t ON t.id = l.task_id + WHERE l.task_id = ? AND l.worker_id = ? + """, + (str(task_id), worker_id), + ).fetchone() + if ( + not row + or not secrets.compare_digest( + str(row["token_hash"]), + _lease_token_hash(lease_token), + ) + ): + conn.commit() + return None + if row["state"] == "completed": + conn.commit() + return dict(row) + valid = ( + row["state"] == "active" + and _parse_timestamp(str(row["lease_expires_at"])) > now + and row["task_status"] == TaskStatus.PROCESSING.value + and row["requested_executor"] == "exe" + and row["assigned_executor"] == worker_id + ) + if not valid: + conn.commit() + return None + updated = conn.execute( + """ + UPDATE task_leases + SET state = 'uploading', + lease_expires_at = ?, + updated_at = ? + WHERE task_id = ? + AND worker_id = ? + AND token_hash = ? + AND state = 'active' + """, + ( + expires_at.isoformat(), + now.isoformat(), + str(task_id), + worker_id, + _lease_token_hash(lease_token), + ), + ) + if updated.rowcount != 1: + conn.rollback() + return None + conn.commit() + except Exception: + conn.rollback() + raise + return { + "task_id": str(task_id), + "worker_id": worker_id, + "state": "uploading", + "lease_expires_at": expires_at.isoformat(), + } + + def begin_remote_finalization( + self, + task_id: UUID | str, + worker_id: str, + lease_token: str, + *, + lease_seconds: int = 3600, + ) -> bool: + """CAS an uploading lease into the non-cancellable publish phase.""" + conn = _get_conn() + now = _utc_now() + expires_at = now + timedelta(seconds=lease_seconds) + with _db_lock: + try: + conn.execute("BEGIN IMMEDIATE") + self._expire_remote_leases_locked(conn, now) + updated = conn.execute( + """ + UPDATE task_leases + SET state = 'finalizing', + lease_expires_at = ?, + updated_at = ? + WHERE task_id = ? + AND worker_id = ? + AND token_hash = ? + AND state = 'uploading' + AND lease_expires_at > ? + AND EXISTS ( + SELECT 1 + FROM tasks + WHERE tasks.id = task_leases.task_id + AND tasks.status = 'processing' + AND tasks.requested_executor = 'exe' + AND tasks.assigned_executor = ? + ) + """, + ( + expires_at.isoformat(), + now.isoformat(), + str(task_id), + worker_id, + _lease_token_hash(lease_token), + now.isoformat(), + worker_id, + ), + ) + if updated.rowcount != 1: + conn.commit() + return False + conn.commit() + except Exception: + conn.rollback() + raise + return True + + def release_remote_upload( + self, + task_id: UUID | str, + worker_id: str, + lease_token: str, + ) -> bool: + """Release an interrupted upload/finalization so the lease can retry.""" + conn = _get_conn() + now = _utc_now() + with _db_lock: + try: + conn.execute("BEGIN IMMEDIATE") + self._expire_remote_leases_locked(conn, now) + updated = conn.execute( + """ + UPDATE task_leases + SET state = 'active', updated_at = ? + WHERE task_id = ? + AND worker_id = ? + AND token_hash = ? + AND state IN ('uploading', 'finalizing') + AND lease_expires_at > ? + AND EXISTS ( + SELECT 1 + FROM tasks + WHERE tasks.id = task_leases.task_id + AND tasks.status = 'processing' + AND tasks.requested_executor = 'exe' + AND tasks.assigned_executor = ? + ) + """, + ( + now.isoformat(), + str(task_id), + worker_id, + _lease_token_hash(lease_token), + now.isoformat(), + worker_id, + ), + ) + conn.commit() + except Exception: + conn.rollback() + raise + return updated.rowcount == 1 + + def transition_task_control( + self, + task_id: UUID, + status: TaskStatus, + *, + flow: dict[str, Any] | None = None, + ) -> Task | None: + """Atomically cancel/pause a task and terminate its mutable EXE lease.""" + if status == TaskStatus.CANCELLED: + allowed = { + TaskStatus.PENDING.value, + TaskStatus.QUEUED.value, + TaskStatus.PROCESSING.value, + TaskStatus.PAUSED.value, + } + message = "已取消" + lease_state = "cancelled" + elif status == TaskStatus.PAUSED: + allowed = { + TaskStatus.PENDING.value, + TaskStatus.QUEUED.value, + TaskStatus.PROCESSING.value, + } + message = "已暂停" + lease_state = "paused" + else: + raise ValueError("Task control transition only supports cancelled or paused") + + conn = _get_conn() + now = _utc_now() + with _db_lock: + try: + conn.execute("BEGIN IMMEDIATE") + self._expire_remote_leases_locked(conn, now) + current = conn.execute( + """ + SELECT status, requested_executor + FROM tasks + WHERE id = ? + """, + (str(task_id),), + ).fetchone() + if not current or current["status"] not in allowed: + conn.commit() + return None + + if current["requested_executor"] == "exe": + lease = conn.execute( + "SELECT state FROM task_leases WHERE task_id = ?", + (str(task_id),), + ).fetchone() + if lease and lease["state"] == "finalizing": + conn.commit() + return None + conn.execute( + """ + UPDATE task_leases + SET state = ?, updated_at = ? + WHERE task_id = ? + AND state IN ('active', 'uploading') + """, + (lease_state, now.isoformat(), str(task_id)), + ) + + values: list[Any] = [ + status.value, + message, + now.isoformat(), + now.isoformat() if status == TaskStatus.CANCELLED else None, + ] + flow_assignment = "" + if status == TaskStatus.PAUSED: + flow_assignment = ", flow = ?" + values.append(json.dumps(flow, ensure_ascii=False) if flow else None) + values.append(str(task_id)) + placeholders = ", ".join("?" for _ in allowed) + values.extend(sorted(allowed)) + updated = conn.execute( + f""" + UPDATE tasks + SET status = ?, + message = ?, + updated_at = ?, + completed_at = ?, + sync_revision = sync_revision + 1 + {flow_assignment} + WHERE id = ? + AND status IN ({placeholders}) + """, + values, + ) + if updated.rowcount != 1: + conn.rollback() + return None + row = conn.execute( + "SELECT * FROM tasks WHERE id = ?", + (str(task_id),), + ).fetchone() + conn.commit() + except Exception: + conn.rollback() + raise + return _row_to_task(row) if row else None + + def complete_remote_task( + self, + task_id: UUID | str, + worker_id: str, + lease_token: str, + result: dict[str, Any], + ) -> tuple[Task | None, bool]: + """Finalize a leased task. Returns ``(task, already_completed)``.""" + conn = _get_conn() + now = _utc_now() + with _db_lock: + try: + conn.execute("BEGIN IMMEDIATE") + lease = conn.execute( + """ + SELECT token_hash, state, lease_expires_at + FROM task_leases + WHERE task_id = ? AND worker_id = ? + """, + (str(task_id), worker_id), + ).fetchone() + if ( + not lease + or not secrets.compare_digest( + str(lease["token_hash"]), + _lease_token_hash(lease_token), + ) + ): + conn.rollback() + return None, False + if lease["state"] == "completed": + row = conn.execute( + "SELECT * FROM tasks WHERE id = ?", + (str(task_id),), + ).fetchone() + conn.commit() + return (_row_to_task(row) if row else None), True + if ( + lease["state"] != "finalizing" + or _parse_timestamp(str(lease["lease_expires_at"])) <= now + ): + conn.rollback() + return None, False + + updated = conn.execute( + """ + UPDATE tasks + SET status = 'completed', + assigned_executor = ?, + progress = 1.0, + message = '处理完成', + result = ?, + error = NULL, + completed_at = ?, + updated_at = ?, + sync_revision = sync_revision + 1 + WHERE id = ? + AND requested_executor = 'exe' + AND status = 'processing' + """, + ( + worker_id, + json.dumps(result, ensure_ascii=False), + now.isoformat(), + now.isoformat(), + str(task_id), + ), + ) + if updated.rowcount != 1: + conn.rollback() + return None, False + conn.execute( + """ + UPDATE task_leases + SET state = 'completed', updated_at = ? + WHERE task_id = ? AND worker_id = ? + """, + (now.isoformat(), str(task_id), worker_id), + ) + row = conn.execute( + "SELECT * FROM tasks WHERE id = ?", + (str(task_id),), + ).fetchone() + conn.commit() + except Exception: + conn.rollback() + raise + return (_row_to_task(row) if row else None), False + + def fail_remote_task( + self, + task_id: UUID | str, + worker_id: str, + lease_token: str, + error: str, + ) -> tuple[Task | None, bool]: + """Mark a leased task failed. Repeated calls with the same lease are idempotent.""" + conn = _get_conn() + now = _utc_now() + with _db_lock: + try: + conn.execute("BEGIN IMMEDIATE") + lease = conn.execute( + """ + SELECT token_hash, state, lease_expires_at + FROM task_leases + WHERE task_id = ? AND worker_id = ? + """, + (str(task_id), worker_id), + ).fetchone() + if ( + not lease + or not secrets.compare_digest( + str(lease["token_hash"]), + _lease_token_hash(lease_token), + ) + ): + conn.rollback() + return None, False + if lease["state"] == "failed": + row = conn.execute( + "SELECT * FROM tasks WHERE id = ?", + (str(task_id),), + ).fetchone() + conn.commit() + return (_row_to_task(row) if row else None), True + if ( + lease["state"] != "active" + or _parse_timestamp(str(lease["lease_expires_at"])) <= now + ): + conn.rollback() + return None, False + + updated = conn.execute( + """ + UPDATE tasks + SET status = 'failed', + assigned_executor = ?, + message = 'EXE 处理失败', + error = ?, + completed_at = ?, + updated_at = ?, + sync_revision = sync_revision + 1 + WHERE id = ? + AND requested_executor = 'exe' + AND status = 'processing' + """, + (worker_id, error, now.isoformat(), now.isoformat(), str(task_id)), + ) + if updated.rowcount != 1: + conn.rollback() + return None, False + conn.execute( + """ + UPDATE task_leases + SET state = 'failed', updated_at = ? + WHERE task_id = ? AND worker_id = ? + """, + (now.isoformat(), str(task_id), worker_id), + ) + row = conn.execute( + "SELECT * FROM tasks WHERE id = ?", + (str(task_id),), + ).fetchone() + conn.commit() + except Exception: + conn.rollback() + raise + return (_row_to_task(row) if row else None), False + def update_status( self, task_id: UUID, @@ -253,11 +1280,26 @@ def update_status( ) -> None: """Update task status and optional fields.""" conn = _get_conn() + has_explicit_revision = "sync_revision" in kwargs sets = ["status = ?", "updated_at = ?"] + if not has_explicit_revision: + sets.append("sync_revision = sync_revision + 1") vals: list[Any] = [status, datetime.now().isoformat()] for key, value in kwargs.items(): - if key in ("progress", "message", "error", "current_step", "uploader_id", "platform", "content_subtype"): + if key in ( + "progress", + "message", + "error", + "current_step", + "uploader_id", + "platform", + "content_subtype", + "origin_client", + "requested_executor", + "assigned_executor", + "sync_revision", + ): sets.append(f"{key} = ?") vals.append(value) elif key == "result": @@ -384,16 +1426,27 @@ def get_artifact(self, task_id: UUID | str, filename: str) -> dict[str, Any] | N row = cur.fetchone() return dict(row) if row else None - def get_artifact_by_output_dir(self, output_dir: Path | str, filename: str) -> dict[str, Any] | None: + def get_artifact_by_output_dir( + self, + output_dir: Path | str, + filename: str, + ) -> dict[str, Any] | None: """Resolve a task by its result output_dir/archive.output_dir and return an artifact.""" target = str(Path(output_dir).resolve()) for task in self.list(limit=10000): result = task.result or {} candidates = [ result.get("output_dir"), - (result.get("archive") or {}).get("output_dir") if isinstance(result.get("archive"), dict) else None, + ( + (result.get("archive") or {}).get("output_dir") + if isinstance(result.get("archive"), dict) + else None + ), ] - if any(candidate and str(Path(candidate).resolve()) == target for candidate in candidates): + if any( + candidate and str(Path(candidate).resolve()) == target + for candidate in candidates + ): return self.get_artifact(task.id, filename) return None diff --git a/backend/app/core/model_router.py b/backend/app/core/model_router.py index 9d81271..9b85b07 100644 --- a/backend/app/core/model_router.py +++ b/backend/app/core/model_router.py @@ -19,8 +19,11 @@ _DEFAULT_QWEN3_GGUF_REPO = "ggml-org/Qwen3-ASR-1.7B-GGUF:Q8_0" _DEFAULT_QWEN3_GGUF_ALIAS = "Qwen3-ASR-1.7B" _DEFAULT_DEEPSEEK_API_BASE = "https://api.deepseek.com" -_DEFAULT_SILICONFLOW_VLM_MODEL = "Qwen/Qwen3.5-4B" -_LEGACY_VLM_DEFAULT_MODELS = {"qwen2.5-vl-7b-instruct"} +_DEFAULT_SILICONFLOW_VLM_MODEL = "Qwen/Qwen3-VL-8B-Instruct" +_LEGACY_VLM_DEFAULT_MODELS = { + "qwen2.5-vl-7b-instruct", + "qwen/qwen3.5-4b", +} _MODEL_TYPE_ENDPOINT_PATHS = { "llm": "/chat/completions", diff --git a/backend/app/core/pipeline.py b/backend/app/core/pipeline.py index e7ff92f..3ff97b4 100644 --- a/backend/app/core/pipeline.py +++ b/backend/app/core/pipeline.py @@ -27,6 +27,7 @@ from app.core.source_normalization import normalize_source_input from app.core.source_resolver import SourceFlow, flow_from_metadata, resolve_source_flow from app.models import MediaMetadata, Task, TaskStatus, TaskType +from app.models.task import REMOTE_MIRROR_OPTION logger = logging.getLogger(__name__) @@ -1167,7 +1168,7 @@ async def _run_subtitle_fast_path( Returns the text-related portion of the task result. """ from app.services.recognition.subtitle_processor import process_subtitles - from app.services.analysis import polish_text, summarize_text, generate_mindmap, generate_detail, analyze_content + from app.services.analysis import analyze_content, generate_detail, generate_mindmap, summarize_text # -- SEPARATE: skip (no audio to separate) -- await _raise_if_cancelled(task.id) @@ -2536,13 +2537,15 @@ def _restore_mindmap() -> bool: is_staged = True except ValueError: is_staged = False - if is_staged: + if is_staged and not task.options.get(REMOTE_MIRROR_OPTION): shutil.move(str(source_path), str(dest_source)) try: source_path.parent.rmdir() except OSError: pass else: + # A coordinator task may be reclaimed after an interrupted + # lease, so keep its preferred EXE staging source readable. shutil.copy2(str(source_path), str(dest_source)) title = dest_source.stem diff --git a/backend/app/core/queue.py b/backend/app/core/queue.py index dca6d54..a4aa0fe 100644 --- a/backend/app/core/queue.py +++ b/backend/app/core/queue.py @@ -14,7 +14,6 @@ import asyncio import json import logging -from datetime import datetime from pathlib import Path from typing import Any, Callable, Coroutine from uuid import UUID @@ -26,7 +25,7 @@ set_task_context, set_worker_context, reset_context, task_id_var, worker_var, ) -from app.models.task import TaskStatus +from app.models.task import REMOTE_MIRROR_OPTION, Task, TaskStatus logger = logging.getLogger(__name__) @@ -79,6 +78,7 @@ def __init__(self): self._active_download_ids: set[UUID] = set() self._active_gpu_id: UUID | None = None self._running_tasks: dict[UUID, asyncio.Task] = {} + self._remote_claims: dict[UUID, str] = {} self._running = False # Exposed so pipeline.py can acquire it around GPU-heavy steps. @@ -112,7 +112,31 @@ async def submit(self, task_id: UUID) -> None: """Enqueue a new task — it goes straight to the download queue.""" store = get_task_store() bus = get_event_bus() - store.update_status(task_id, TaskStatus.QUEUED) + task = store.get(task_id) + if not task: + return + if task.options.get(REMOTE_MIRROR_OPTION) or task.requested_executor != "server": + store.update_status( + task_id, + TaskStatus.QUEUED, + message=( + "等待远程处理..." + if task.options.get(REMOTE_MIRROR_OPTION) + else "等待 EXE 处理..." + ), + assigned_executor=task.assigned_executor, + ) + await bus.publish(TaskEvent( + task_id, + "queued", + {"requested_executor": task.requested_executor}, + )) + return + store.update_status( + task_id, + TaskStatus.QUEUED, + assigned_executor="server", + ) await bus.publish(TaskEvent(task_id, "queued")) t_token = set_task_context(str(task_id)) try: @@ -126,16 +150,73 @@ async def submit(self, task_id: UUID) -> None: finally: reset_context(t_token, task_id_var) + async def submit_remote_claim(self, task_id: UUID, worker_id: str) -> None: + """Authorize and enqueue one coordinator-leased task on this EXE.""" + store = get_task_store() + task = store.get(task_id) + if ( + not task + or not task.options.get(REMOTE_MIRROR_OPTION) + or task.requested_executor != "exe" + or task.assigned_executor != worker_id + ): + raise ValueError("Remote task is not assigned to this EXE worker") + + self._remote_claims[task_id] = worker_id + store.update_status( + task_id, + TaskStatus.QUEUED, + message="已领取远程任务,等待本机处理...", + error=None, + ) + await get_event_bus().publish( + TaskEvent( + task_id, + "queued", + { + "requested_executor": "exe", + "assigned_executor": worker_id, + "remote_claim": True, + }, + ) + ) + await self._download_queue.put(task_id) + log_event( + logger, + logging.INFO, + "queue.remote_claim.enqueued", + worker_id=worker_id, + depth=self._download_queue.qsize(), + ) + + def release_remote_claim(self, task_id: UUID, worker_id: str) -> bool: + """Remove an in-memory EXE execution grant after sync finalization.""" + if self._remote_claims.get(task_id) != worker_id: + return False + self._remote_claims.pop(task_id, None) + return True + + def _is_locally_runnable(self, task: Task) -> bool: + if task.options.get(REMOTE_MIRROR_OPTION): + worker_id = self._remote_claims.get(task.id) + return bool( + worker_id + and task.requested_executor == "exe" + and task.assigned_executor == worker_id + ) + return task.requested_executor == "server" + async def cancel(self, task_id: UUID) -> bool: store = get_task_store() task = store.get(task_id) if not task: return False - if task.status not in (TaskStatus.PENDING, TaskStatus.QUEUED, TaskStatus.PROCESSING, TaskStatus.PAUSED): + cancelled = store.transition_task_control(task_id, TaskStatus.CANCELLED) + if not cancelled: return False + task = cancelled self._remove_from_queue(self._download_queue, task_id) self._remove_from_queue(self._gpu_queue, task_id) - store.update_status(task_id, TaskStatus.CANCELLED, completed_at=datetime.now()) if task.result and task.result.get("output_dir"): try: from pathlib import Path @@ -162,8 +243,6 @@ async def pause(self, task_id: UUID) -> bool: task = store.get(task_id) if not task: return False - if task.status not in (TaskStatus.PENDING, TaskStatus.QUEUED, TaskStatus.PROCESSING): - return False self._remove_from_queue(self._download_queue, task_id) self._remove_from_queue(self._gpu_queue, task_id) @@ -172,13 +251,14 @@ async def pause(self, task_id: UUID) -> bool: if flow: flow = dict(flow) flow["status"] = "paused" - store.update_status( + paused = store.transition_task_control( task_id, TaskStatus.PAUSED, - message="已暂停", flow=flow, - completed_at=None, ) + if not paused: + return False + task = paused if task.result and task.result.get("output_dir"): try: from app.core.pipeline import update_metadata_status @@ -461,8 +541,27 @@ async def start(self) -> None: from app.core.pipeline import PipelineStep stale = store.list_by_statuses([TS.QUEUED, TS.PROCESSING]) for task in stale: + # EXE-targeted tasks are leased through the coordinator API. They + # remain visible in the canonical DB while the local GPU queue only + # restores tasks assigned to this server. + if ( + task.options.get(REMOTE_MIRROR_OPTION) + or task.requested_executor != "server" + ): + continue if task.status == TS.PROCESSING: - store.update_status(task.id, TS.QUEUED, message="已重新排队") + store.update_status( + task.id, + TS.QUEUED, + message="已重新排队", + assigned_executor="server", + ) + elif task.assigned_executor != "server": + store.update_status( + task.id, + TS.QUEUED, + assigned_executor="server", + ) # If download was already done, skip straight to GPU queue completed = set(task.completed_steps or []) if PipelineStep.DOWNLOAD in completed: @@ -539,6 +638,7 @@ async def stop(self) -> None: pass self._gpu_worker_task = None + self._remote_claims.clear() log_event(logger, logging.INFO, "queue.stopped") # ------------------------------------------------------------------ @@ -575,7 +675,11 @@ async def _download_worker(self, worker_index: int = 0) -> None: store = get_task_store() task = store.get(task_id) - if not task or task.status == TaskStatus.CANCELLED: + if ( + not task + or task.status == TaskStatus.CANCELLED + or not self._is_locally_runnable(task) + ): self._download_queue.task_done() continue @@ -615,7 +719,11 @@ async def _gpu_worker(self) -> None: store = get_task_store() task = store.get(task_id) - if not task or task.status == TaskStatus.CANCELLED: + if ( + not task + or task.status == TaskStatus.CANCELLED + or not self._is_locally_runnable(task) + ): self._gpu_queue.task_done() continue diff --git a/backend/app/core/settings.py b/backend/app/core/settings.py index 8456dae..2ac8eb3 100644 --- a/backend/app/core/settings.py +++ b/backend/app/core/settings.py @@ -312,7 +312,7 @@ class RuntimeSettings(BaseModel): # VLM (image understanding) — OpenAI-Compatible API vlm_api_base: str = "" vlm_api_key: str = "" - vlm_model: str = "Qwen/Qwen3.5-4B" + vlm_model: str = "Qwen/Qwen3-VL-8B-Instruct" vlm_max_tokens: int = 1024 vlm_concurrency: int = 1 vlm_timeout_sec: float = 180.0 @@ -339,6 +339,18 @@ class RuntimeSettings(BaseModel): # Security api_token: str = "" # Bearer token for API auth; empty = auth disabled + # Remote hub synchronization (desktop EXE worker) + remote_sync_enabled: bool = False + remote_server_url: str = "" + remote_api_token: str = "" + remote_worker_id: str = "" + remote_worker_name: str = "" + remote_sync_interval_sec: float = 15.0 + remote_sync_upload_results: bool = True + remote_sync_download_results: bool = True + remote_sync_include_media: bool = False + default_task_executor: str = "server" # server | exe + # Paths data_root: str = "D:/Video/MediaProcessPipeline" @@ -441,6 +453,21 @@ def _validate_bilibili_subtitle_min_coverage(cls, value: float) -> float: raise ValueError("bilibili_subtitle_min_coverage must be between 0 and 1") return value + @field_validator("remote_sync_interval_sec") + @classmethod + def _validate_remote_sync_interval(cls, value: float) -> float: + if value < 5: + raise ValueError("remote_sync_interval_sec must be at least 5 seconds") + return value + + @field_validator("default_task_executor") + @classmethod + def _validate_default_task_executor(cls, value: str) -> str: + executor = value.strip().lower() + if executor not in {"server", "exe"}: + raise ValueError("default_task_executor must be one of: server, exe") + return executor + # Global runtime settings storage _runtime_settings: RuntimeSettings | None = None diff --git a/backend/app/main.py b/backend/app/main.py index 2c5f2a1..589128d 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,6 +1,7 @@ import logging import sys import asyncio +import os from contextlib import asynccontextmanager from pathlib import Path @@ -9,13 +10,12 @@ from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse, JSONResponse -from app.api.routes import tasks, pipeline, filesystem, voiceprints +from app.api.routes import tasks, pipeline, filesystem, voiceprints, sync from app.api.routes import settings as settings_router from app.api.routes import kb as kb_router from app.core.settings import get_runtime_settings, SETTINGS_FILE from app.core.config import get_settings from app.core.database import init_db, close_db -from app.core.events import get_event_bus from app.core.logging_setup import setup_logging from app.core.pipeline import process_task from app.core.queue import get_task_queue @@ -50,6 +50,10 @@ async def get_response(self, path: str, scope): async def lifespan(app: FastAPI): # Startup rt = get_runtime_settings() + if _api_token_is_required() and not rt.api_token: + raise RuntimeError( + "MPP_REQUIRE_API_TOKEN is enabled, but api_token is empty or unavailable" + ) logger.info(f"Loaded runtime settings from {SETTINGS_FILE}") logger.info(f" LLM Provider: {rt.llm_provider}") if rt.llm_provider == "custom": @@ -65,11 +69,29 @@ async def lifespan(app: FastAPI): # Initialize SQLite task store init_db() + try: + from app.core.archive_sync import sweep_stale_sync_storage + + sync_cleanup = sweep_stale_sync_storage(Path(rt.data_root)) + if sync_cleanup["removed"] or sync_cleanup["restored"]: + logger.info( + "Sync storage recovery: removed=%s restored=%s", + sync_cleanup["removed"], + sync_cleanup["restored"], + ) + except Exception as e: + logger.warning("Sync storage recovery failed: %s", e) + # Start task queue worker queue = get_task_queue() queue.set_pipeline(process_task) await queue.start() + from app.services.remote_sync import get_remote_sync_service + + remote_sync = get_remote_sync_service() + await remote_sync.start() + # Sweep stale upload staging dirs (>24h old, never confirmed by user) try: from app.api.routes.pipeline import sweep_stale_staging @@ -86,17 +108,20 @@ async def lifespan(app: FastAPI): # closing the database. The desktop process job remains the hard-stop # fallback for interrupted Windows exits. try: - await queue.stop() + await remote_sync.stop() finally: try: - from app.services.recognition import release_asr_models - from app.services.analysis.local_llm_runtime import release_local_llm_runtime - - await asyncio.to_thread(release_asr_models) - await asyncio.to_thread(release_local_llm_runtime) - except Exception as e: - logger.warning("Runtime cleanup during shutdown failed: %s", e) - close_db() + await queue.stop() + finally: + try: + from app.services.recognition import release_asr_models + from app.services.analysis.local_llm_runtime import release_local_llm_runtime + + await asyncio.to_thread(release_asr_models) + await asyncio.to_thread(release_local_llm_runtime) + except Exception as e: + logger.warning("Runtime cleanup during shutdown failed: %s", e) + close_db() app = FastAPI( title=config.api_title, @@ -124,14 +149,29 @@ async def lifespan(app: FastAPI): _AUTH_EXEMPT_PREFIXES = ("/health", "/assets", "/favicon") +def _api_token_is_required() -> bool: + return os.environ.get("MPP_REQUIRE_API_TOKEN", "").strip().lower() in { + "1", + "true", + "yes", + "on", + } + + @app.middleware("http") async def auth_middleware(request: Request, call_next): path = request.url.path # Only gate /api/* endpoints if path.startswith("/api"): - # Bearer token auth (optional — only when api_token is configured) + # Bearer token auth. Production coordinators can force fail-closed + # behavior independently of the runtime settings file. rt = get_runtime_settings() token = rt.api_token + if _api_token_is_required() and not token: + return JSONResponse( + status_code=503, + content={"detail": "API authentication is required but unavailable"}, + ) if token: auth_header = request.headers.get("authorization", "") cookie_token = request.cookies.get("mpp_api_token", "") @@ -161,6 +201,7 @@ async def auth_middleware(request: Request, call_next): app.include_router(filesystem.router, prefix="/api") app.include_router(voiceprints.router, prefix="/api") app.include_router(kb_router.router, prefix="/api") +app.include_router(sync.router, prefix="/api") @app.get("/health") diff --git a/backend/app/models/task.py b/backend/app/models/task.py index ec0195a..bca3f46 100644 --- a/backend/app/models/task.py +++ b/backend/app/models/task.py @@ -2,11 +2,15 @@ from datetime import datetime from enum import StrEnum -from typing import Any +from typing import Any, Literal from uuid import UUID, uuid4 from pydantic import BaseModel, Field +PREFERRED_WORKER_OPTION = "_mpp_preferred_worker_id" +REMOTE_MIRROR_OPTION = "_mpp_remote_mirror" +REMOTE_ARCHIVE_REVISION_OPTION = "_mpp_remote_archive_revision" + class TaskStatus(StrEnum): PENDING = "pending" @@ -73,6 +77,8 @@ class TaskCreate(BaseModel): source: str options: dict[str, Any] = Field(default_factory=dict) webhook_url: str | None = None + origin_client: str = Field(default="api", min_length=1, max_length=64) + requested_executor: Literal["server", "exe"] = "server" class TaskBatchCreate(BaseModel): @@ -80,6 +86,8 @@ class TaskBatchCreate(BaseModel): sources: list[str] = Field(min_length=1, max_length=100) options: dict[str, Any] = Field(default_factory=dict) webhook_url: str | None = None + origin_client: str = Field(default="api", min_length=1, max_length=64) + requested_executor: Literal["server", "exe"] = "server" class Task(BaseModel): @@ -105,3 +113,9 @@ class Task(BaseModel): platform: str | None = None uploader_id: str | None = None content_subtype: str | None = None + # Coordinator metadata. The server owns the canonical task record while + # desktop workers lease tasks whose requested executor is ``exe``. + origin_client: str = "legacy" + requested_executor: Literal["server", "exe"] = "server" + assigned_executor: str | None = None + sync_revision: int = Field(default=0, ge=0) diff --git a/backend/app/services/analysis/vlm.py b/backend/app/services/analysis/vlm.py index ee0e23c..6cd881c 100644 --- a/backend/app/services/analysis/vlm.py +++ b/backend/app/services/analysis/vlm.py @@ -161,7 +161,7 @@ def describe_image( b64, media_type, payload_meta = _encode_image(image_path) timeout_sec = int(binding.request_kwargs.get("timeout_sec") or 90) started = time.monotonic() - max_tokens = max(2048, int(binding.request_kwargs.get("max_tokens") or rt.vlm_max_tokens)) + max_tokens = max(256, int(binding.request_kwargs.get("max_tokens") or rt.vlm_max_tokens)) response = client.chat.completions.create( model=model, max_tokens=max_tokens, diff --git a/backend/app/services/archiving/thumbnails.py b/backend/app/services/archiving/thumbnails.py index 8dc68dd..9fffb64 100644 --- a/backend/app/services/archiving/thumbnails.py +++ b/backend/app/services/archiving/thumbnails.py @@ -4,6 +4,9 @@ import io import logging +import shutil +import subprocess +import warnings from pathlib import Path logger = logging.getLogger(__name__) @@ -21,12 +24,201 @@ DEFAULT_THUMBNAIL_SIZE = (480, 480) DEFAULT_THUMBNAIL_QUALITY = 82 +MAX_ARCHIVE_IMAGE_SOURCE_BYTES = 64 * 1024 * 1024 +MAX_ARCHIVE_IMAGE_PIXELS = 64_000_000 +MAX_ARCHIVE_IMAGE_OUTPUT_BYTES = 16 * 1024 * 1024 +SUPPORTED_ARCHIVE_IMAGE_FORMATS = { + "AVIF", + "BMP", + "GIF", + "JPEG", + "PNG", + "WEBP", +} + + +class ArchiveImageResizeError(ValueError): + """Base error raised when an archive image cannot be delivered safely.""" + + +class ArchiveImageTooLargeError(ArchiveImageResizeError): + """The source pixels/file or resulting JPEG exceeds the configured limit.""" + + +class ArchiveImageFormatError(ArchiveImageResizeError): + """The source is not a supported, decodable image.""" + + +class PillowUnavailableError(ArchiveImageResizeError): + """Pillow is required for the archive image delivery endpoint.""" + + +def _remove_partial_thumbnail(thumb_path: Path) -> None: + try: + thumb_path.unlink(missing_ok=True) + except OSError: + pass + + +def _create_pillow_thumbnail_from_bytes( + data: bytes, + thumb_path: Path, + *, + size: tuple[int, int], + quality: int, +) -> bool: + try: + from PIL import Image, ImageOps + except ImportError: + return False + + try: + with Image.open(io.BytesIO(data)) as image: + image = ImageOps.exif_transpose(image) + if image.mode not in {"RGB", "L"}: + image = image.convert("RGB") + image.thumbnail(size, Image.Resampling.LANCZOS) + image.save(thumb_path, "JPEG", quality=quality, optimize=True) + return thumb_path.is_file() and thumb_path.stat().st_size > 0 + except Exception as exc: + _remove_partial_thumbnail(thumb_path) + logger.debug("Pillow image thumbnail conversion failed: %s", exc) + return False + + +def _create_ffmpeg_thumbnail_from_bytes( + data: bytes, + thumb_path: Path, + *, + size: tuple[int, int], +) -> bool: + ffmpeg = shutil.which("ffmpeg") + if not ffmpeg: + return False + + width, height = size + try: + result = subprocess.run( + [ + ffmpeg, + "-v", + "error", + "-y", + "-i", + "pipe:0", + "-vf", + f"scale={width}:{height}:force_original_aspect_ratio=decrease", + "-frames:v", + "1", + "-q:v", + "5", + str(thumb_path), + ], + input=data, + capture_output=True, + timeout=30, + check=False, + ) + if result.returncode == 0 and thumb_path.is_file() and thumb_path.stat().st_size > 0: + return True + _remove_partial_thumbnail(thumb_path) + logger.debug( + "ffmpeg image thumbnail conversion failed: returncode=%s stderr=%s", + result.returncode, + result.stderr.decode("utf-8", errors="replace").strip(), + ) + except Exception as exc: + _remove_partial_thumbnail(thumb_path) + logger.debug("ffmpeg image thumbnail conversion failed: %s", exc) + return False def image_media_type(path: Path) -> str: return IMAGE_MEDIA_TYPES.get(path.suffix.lower(), "application/octet-stream") +def resize_archive_image_to_jpeg( + source: Path, + *, + max_edge: int, + quality: int, +) -> bytes: + """Decode one bounded archive image and return an oriented, resized JPEG.""" + if not 256 <= max_edge <= 4096: + raise ArchiveImageResizeError("max_edge must be between 256 and 4096") + if not 55 <= quality <= 90: + raise ArchiveImageResizeError("quality must be between 55 and 90") + try: + source_size = source.stat().st_size + except OSError as exc: + raise ArchiveImageFormatError("Archive image cannot be read") from exc + if source_size <= 0: + raise ArchiveImageFormatError("Archive image is empty") + if source_size > MAX_ARCHIVE_IMAGE_SOURCE_BYTES: + raise ArchiveImageTooLargeError( + f"Archive image exceeds {MAX_ARCHIVE_IMAGE_SOURCE_BYTES} bytes", + ) + + try: + from PIL import Image, ImageOps + except ImportError as exc: + raise PillowUnavailableError( + "Pillow is required to resize archive images", + ) from exc + + try: + with warnings.catch_warnings(): + warnings.simplefilter("error", Image.DecompressionBombWarning) + with Image.open(source) as opened: + image_format = (opened.format or "").upper() + if image_format not in SUPPORTED_ARCHIVE_IMAGE_FORMATS: + raise ArchiveImageFormatError( + f"Unsupported archive image format: {image_format or 'unknown'}", + ) + width, height = opened.size + if width <= 0 or height <= 0: + raise ArchiveImageFormatError("Archive image has invalid dimensions") + if width * height > MAX_ARCHIVE_IMAGE_PIXELS: + raise ArchiveImageTooLargeError( + f"Archive image exceeds {MAX_ARCHIVE_IMAGE_PIXELS} pixels", + ) + + opened.seek(0) + image = ImageOps.exif_transpose(opened) + image.thumbnail((max_edge, max_edge), Image.Resampling.LANCZOS) + if image.mode in {"RGBA", "LA"} or "transparency" in image.info: + rgba = image.convert("RGBA") + flattened = Image.new("RGB", rgba.size, "white") + flattened.paste(rgba, mask=rgba.getchannel("A")) + image = flattened + elif image.mode != "RGB": + image = image.convert("RGB") + + output = io.BytesIO() + image.save( + output, + "JPEG", + quality=quality, + optimize=True, + progressive=True, + ) + payload = output.getvalue() + except ArchiveImageResizeError: + raise + except (Image.DecompressionBombError, Image.DecompressionBombWarning) as exc: + raise ArchiveImageTooLargeError("Archive image pixel count is unsafe") from exc + except (OSError, SyntaxError, ValueError) as exc: + raise ArchiveImageFormatError("Archive image cannot be decoded") from exc + + if not payload: + raise ArchiveImageFormatError("Archive image produced an empty JPEG") + if len(payload) > MAX_ARCHIVE_IMAGE_OUTPUT_BYTES: + raise ArchiveImageTooLargeError( + f"Resized JPEG exceeds {MAX_ARCHIVE_IMAGE_OUTPUT_BYTES} bytes", + ) + return payload + + def first_image_note_image(archive_dir: Path) -> Path | None: image_dir = archive_dir / "images" if not image_dir.is_dir(): @@ -44,23 +236,17 @@ def create_image_thumbnail_from_bytes( size: tuple[int, int] = DEFAULT_THUMBNAIL_SIZE, quality: int = DEFAULT_THUMBNAIL_QUALITY, ) -> Path | None: - try: - from PIL import Image, ImageOps - except ImportError: - return None - thumb_path = archive_dir / "thumbnail.jpg" - try: - with Image.open(io.BytesIO(data)) as image: - image = ImageOps.exif_transpose(image) - if image.mode not in {"RGB", "L"}: - image = image.convert("RGB") - image.thumbnail(size, Image.Resampling.LANCZOS) - image.save(thumb_path, "JPEG", quality=quality, optimize=True) - return thumb_path if thumb_path.exists() else None - except Exception as exc: - logger.debug("image thumbnail conversion failed: %s", exc) - return None + if _create_pillow_thumbnail_from_bytes( + data, + thumb_path, + size=size, + quality=quality, + ): + return thumb_path + if _create_ffmpeg_thumbnail_from_bytes(data, thumb_path, size=size): + return thumb_path + return None def create_image_thumbnail( diff --git a/backend/app/services/remote_sync.py b/backend/app/services/remote_sync.py new file mode 100644 index 0000000..8e2ea25 --- /dev/null +++ b/backend/app/services/remote_sync.py @@ -0,0 +1,1172 @@ +"""Desktop EXE client for coordinator-backed task and archive synchronization.""" + +from __future__ import annotations + +import asyncio +import json +import logging +import os +import re +import shutil +import socket +import time +from contextlib import suppress +from dataclasses import dataclass, field +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Callable +from urllib.parse import quote, urlparse +from uuid import UUID, uuid4 + +import httpx + +from app.core.archive_sync import ( + MAX_PORTABLE_RESULT_JSON_BYTES, + MAX_UPLOAD_BYTES, + archive_file_manifest, + build_archive_zip, + publish_archive, + rewrite_archive_image_paths, + rewrite_result_image_paths, + safe_extract_zip, + sanitize_portable_result, +) +from app.core.database import TaskStore, get_task_store +from app.core.logging_setup import log_event +from app.core.network import httpx_client_kwargs +from app.core.queue import TaskQueue, get_task_queue +from app.core.settings import ( + RuntimeSettings, + get_runtime_settings, + patch_runtime_settings, +) +from app.models import Task, TaskCreate, TaskStatus, TaskType +from app.models.task import ( + PREFERRED_WORKER_OPTION, + REMOTE_ARCHIVE_REVISION_OPTION, + REMOTE_MIRROR_OPTION, +) + +logger = logging.getLogger(__name__) + +_LEASE_SECONDS = 900 +_MIRROR_LIMIT = 200 +_TERMINAL_STATUSES = { + TaskStatus.COMPLETED, + TaskStatus.FAILED, + TaskStatus.CANCELLED, +} + + +class RemoteSyncConfigurationError(ValueError): + """Raised when desktop coordinator settings are incomplete or unsafe.""" + + +class RemoteSyncUnavailableError(RuntimeError): + """Raised when a foreground task cannot reach the configured coordinator.""" + + +class _LeaseLostError(RuntimeError): + pass + + +@dataclass(frozen=True) +class _RemoteConfig: + api_base: str + api_token: str = field(repr=False) + worker_id: str + worker_name: str + interval_sec: float + upload_results: bool + download_results: bool + include_media: bool + + @classmethod + def from_settings(cls, settings: RuntimeSettings) -> _RemoteConfig: + raw_url = str(settings.remote_server_url or "").strip() + parsed = urlparse(raw_url) + if parsed.scheme not in {"http", "https"} or not parsed.netloc: + raise RemoteSyncConfigurationError( + "远程服务器地址必须是完整的 HTTP 或 HTTPS 地址" + ) + if parsed.username or parsed.password: + raise RemoteSyncConfigurationError("远程服务器地址不能包含用户名或密码") + + base = raw_url.rstrip("/") + api_base = base if base.endswith("/api") else f"{base}/api" + return cls( + api_base=f"{api_base}/", + api_token=str(settings.remote_api_token or ""), + worker_id=str(settings.remote_worker_id or "").strip(), + worker_name=( + str(settings.remote_worker_name or "").strip() + or socket.gethostname().strip() + or "MPP EXE" + ), + interval_sec=max(5.0, float(settings.remote_sync_interval_sec)), + upload_results=bool(settings.remote_sync_upload_results), + download_results=bool(settings.remote_sync_download_results), + include_media=bool(settings.remote_sync_include_media), + ) + + @property + def registration_key(self) -> tuple[str, str, str]: + return self.api_base, self.worker_id, self.worker_name + + +def _is_http_source(source: str) -> bool: + return urlparse(str(source or "").strip()).scheme.lower() in {"http", "https"} + + +def _parse_lease_expiry(value: Any) -> datetime: + try: + parsed = datetime.fromisoformat(str(value)) + except (TypeError, ValueError): + return datetime.now(timezone.utc) + return parsed if parsed.tzinfo is not None else parsed.replace(tzinfo=timezone.utc) + + +def _safe_remote_error(response: httpx.Response) -> str: + try: + payload = response.json() + except ValueError: + payload = None + if isinstance(payload, dict) and isinstance(payload.get("detail"), str): + return payload["detail"][:500] + return f"HTTP {response.status_code}" + + +def _safe_remote_input_name(raw_name: str, task_id: UUID) -> str: + name = Path(str(raw_name or "")).name + name = re.sub(r'[<>:"/\\|?*\x00-\x1f]', "_", name).strip(" .") + return name[:200] or f"{task_id}.media" + + +class RemoteSyncService: + """Coordinates one desktop daemon with the canonical server.""" + + def __init__( + self, + *, + settings_getter: Callable[[], RuntimeSettings] = get_runtime_settings, + store_getter: Callable[[], TaskStore] = get_task_store, + queue_getter: Callable[[], TaskQueue] = get_task_queue, + ) -> None: + self._settings_getter = settings_getter + self._store_getter = store_getter + self._queue_getter = queue_getter + self._runner: asyncio.Task[None] | None = None + self._registered_key: tuple[str, str, str] | None = None + self._registered_worker_id = "" + self._active_task_id: UUID | None = None + + @property + def active_task_id(self) -> UUID | None: + return self._active_task_id + + async def start(self) -> None: + if self._runner and not self._runner.done(): + return + self._runner = asyncio.create_task(self._run(), name="remote-sync") + + async def stop(self) -> None: + runner = self._runner + self._runner = None + if not runner: + return + runner.cancel() + with suppress(asyncio.CancelledError): + await runner + + def _make_client(self, config: _RemoteConfig) -> httpx.AsyncClient: + headers = { + "User-Agent": "MPP-EXE-Remote-Sync/1", + "X-Requested-With": "MPP-Remote-Sync", + } + if config.api_token: + headers["Authorization"] = f"Bearer {config.api_token}" + return httpx.AsyncClient( + base_url=config.api_base, + headers=headers, + timeout=httpx.Timeout(60.0, connect=15.0), + follow_redirects=False, + **httpx_client_kwargs(config.api_base), + ) + + async def _wait_or_stop(self, seconds: float) -> None: + await asyncio.sleep(seconds) + + async def _run(self) -> None: + while True: + settings = self._settings_getter() + if not settings.remote_sync_enabled: + self._registered_key = None + self._registered_worker_id = "" + await self._wait_or_stop(2.0) + continue + + try: + config = _RemoteConfig.from_settings(settings) + async with self._make_client(config) as client: + worker_id = await self._ensure_registered(client, config) + await self._heartbeat(client, worker_id) + if config.upload_results: + claim = await self._claim(client, worker_id) + if claim: + await self._process_claim(client, config, worker_id, claim) + await self._mirror_remote_tasks(client, config) + except asyncio.CancelledError: + raise + except RemoteSyncConfigurationError as exc: + log_event( + logger, + logging.WARNING, + "remote_sync.configuration_invalid", + error=exc, + ) + except httpx.HTTPStatusError as exc: + log_event( + logger, + logging.WARNING, + "remote_sync.request_failed", + status_code=exc.response.status_code, + ) + if exc.response.status_code == 404: + self._registered_key = None + self._registered_worker_id = "" + except httpx.RequestError as exc: + log_event( + logger, + logging.WARNING, + "remote_sync.connection_failed", + error_type=type(exc).__name__, + ) + except Exception as exc: + log_event( + logger, + logging.ERROR, + "remote_sync.cycle_failed", + error_type=type(exc).__name__, + exc_info=True, + ) + + interval = max( + 5.0, + float(self._settings_getter().remote_sync_interval_sec), + ) + await self._wait_or_stop(interval) + + async def _ensure_registered( + self, + client: httpx.AsyncClient, + config: _RemoteConfig, + ) -> str: + if self._registered_key == config.registration_key and self._registered_worker_id: + return self._registered_worker_id + + response = await client.post( + "workers/register", + json={ + "worker_id": config.worker_id or None, + "name": config.worker_name, + "capabilities": { + "executor": "exe", + "task_types": ["pipeline"], + "portable_archives": True, + "upload_results": config.upload_results, + "download_results": config.download_results, + }, + }, + ) + response.raise_for_status() + payload = response.json() + worker = payload.get("worker") if isinstance(payload, dict) else None + worker_id = str(worker.get("id") if isinstance(worker, dict) else "").strip() + if not worker_id: + raise RemoteSyncUnavailableError("协调服务器返回了无效的 worker_id") + + if not config.worker_id: + patch_runtime_settings({"remote_worker_id": worker_id}) + self._registered_worker_id = worker_id + self._registered_key = ( + config.api_base, + worker_id, + config.worker_name, + ) + log_event(logger, logging.INFO, "remote_sync.worker_registered", worker_id=worker_id) + return worker_id + + async def _heartbeat(self, client: httpx.AsyncClient, worker_id: str) -> None: + response = await client.post( + f"workers/{quote(worker_id, safe='')}/heartbeat", + json={"capabilities": None}, + ) + response.raise_for_status() + + async def _claim( + self, + client: httpx.AsyncClient, + worker_id: str, + ) -> dict[str, Any] | None: + response = await client.post( + f"workers/{quote(worker_id, safe='')}/claim", + json={"lease_seconds": _LEASE_SECONDS}, + ) + response.raise_for_status() + payload = response.json() + if not isinstance(payload, dict) or payload.get("task") is None: + return None + return payload + + async def _process_claim( + self, + client: httpx.AsyncClient, + config: _RemoteConfig, + worker_id: str, + claim: dict[str, Any], + ) -> None: + remote_task = Task.model_validate(claim.get("task")) + lease_token = str(claim.get("lease_token") or "") + if len(lease_token) < 16: + raise RemoteSyncUnavailableError("协调服务器返回了无效的任务租约") + if remote_task.task_type != TaskType.PIPELINE: + await self._report_failure( + client, + config, + worker_id, + remote_task.id, + lease_token, + "EXE 远程处理仅支持 pipeline 任务", + ) + return + + queue = self._queue_getter() + self._active_task_id = remote_task.id + lease_lost = asyncio.Event() + lease_guard = asyncio.create_task( + self._maintain_lease( + client, + config, + worker_id, + remote_task.id, + lease_token, + claim.get("lease_expires_at"), + lease_lost, + ), + name=f"remote-lease-{remote_task.id}", + ) + mirror_guard = asyncio.create_task( + self._maintain_remote_mirror(client, config, lease_lost), + name=f"remote-mirror-{remote_task.id}", + ) + local_task: Task | None = None + + try: + local_task = await self._materialize_claimed_source( + client, + config, + worker_id, + remote_task, + claim.get("input"), + lease_token, + lease_lost, + ) + store = self._store_getter() + store.save_remote_mirror(local_task) + await queue.submit_remote_claim(local_task.id, worker_id) + completed = await self._wait_for_local_terminal(local_task.id, lease_lost) + if completed is None: + await queue.cancel(local_task.id) + raise _LeaseLostError("Remote task lease expired") + + if completed.status == TaskStatus.COMPLETED: + payload = await self._upload_completed_task( + client, + config, + worker_id, + completed, + lease_token, + lease_lost, + ) + self._apply_remote_revision(completed, payload) + await asyncio.to_thread(self._cleanup_staged_source, completed) + else: + error = completed.error or ( + "EXE 本地任务已取消" + if completed.status == TaskStatus.CANCELLED + else "EXE 本地任务处理失败" + ) + await self._report_failure( + client, + config, + worker_id, + completed.id, + lease_token, + error, + lease_lost=lease_lost, + ) + except (OSError, ValueError, RemoteSyncUnavailableError) as exc: + await self._report_failure( + client, + config, + worker_id, + remote_task.id, + lease_token, + f"EXE 无法获取远程任务输入: {exc}", + lease_lost=lease_lost, + ) + except _LeaseLostError: + log_event( + logger, + logging.WARNING, + "remote_sync.lease_lost", + task_id=remote_task.id, + worker_id=worker_id, + ) + finally: + for guard in (mirror_guard, lease_guard): + guard.cancel() + for guard in (mirror_guard, lease_guard): + with suppress(asyncio.CancelledError): + await guard + queue.release_remote_claim(remote_task.id, worker_id) + self._active_task_id = None + + async def _materialize_claimed_source( + self, + client: httpx.AsyncClient, + config: _RemoteConfig, + worker_id: str, + task: Task, + input_info: Any, + lease_token: str, + lease_lost: asyncio.Event, + ) -> Task: + if _is_http_source(task.source): + return self._claimed_task_for_local(task, worker_id) + if isinstance(input_info, dict): + local_source = await self._download_claimed_input( + client, + config, + worker_id, + task, + input_info, + lease_token, + lease_lost, + ) + materialized = task.model_copy(deep=True) + materialized.source = str(local_source) + return self._claimed_task_for_local(materialized, worker_id) + if Path(task.source).is_file(): + return self._claimed_task_for_local(task, worker_id) + raise RemoteSyncUnavailableError( + "任务既没有可读取的 URL、本机文件,也没有服务器暂存输入" + ) + + async def _download_claimed_input( + self, + client: httpx.AsyncClient, + config: _RemoteConfig, + worker_id: str, + task: Task, + input_info: dict[str, Any], + lease_token: str, + lease_lost: asyncio.Event, + ) -> Path: + filename = _safe_remote_input_name( + str(input_info.get("filename") or task.source), + task.id, + ) + try: + expected_size = int(input_info.get("size")) + except (TypeError, ValueError) as exc: + raise RemoteSyncUnavailableError("协调服务器返回了无效的输入大小") from exc + if expected_size < 0 or expected_size > MAX_UPLOAD_BYTES: + raise RemoteSyncUnavailableError("远程任务输入超过 10 GB 下载上限") + + data_root = Path(self._settings_getter().data_root).resolve() + staging_dir = ( + data_root / "_staging" / f"remote-{task.id}-{uuid4().hex}" + ) + destination = staging_dir / filename + partial = staging_dir / f".{filename}.{uuid4().hex}.part" + staging_dir.mkdir(parents=True, exist_ok=False) + try: + while not lease_lost.is_set(): + try: + await self._stream_claimed_input_download( + client, + worker_id, + task.id, + lease_token, + partial, + expected_size, + lease_lost, + ) + await asyncio.to_thread(os.replace, partial, destination) + return destination + except httpx.RequestError: + partial.unlink(missing_ok=True) + try: + await asyncio.wait_for( + lease_lost.wait(), + timeout=config.interval_sec, + ) + except asyncio.TimeoutError: + continue + raise _LeaseLostError("Lease expired before task input download completed") + except Exception: + await asyncio.to_thread(shutil.rmtree, staging_dir, True) + raise + + async def _stream_claimed_input_download( + self, + client: httpx.AsyncClient, + worker_id: str, + task_id: UUID, + lease_token: str, + destination: Path, + expected_size: int, + lease_lost: asyncio.Event, + ) -> None: + async with client.stream( + "GET", + f"workers/{quote(worker_id, safe='')}/tasks/{task_id}/input", + headers={"X-MPP-Lease-Token": lease_token}, + timeout=httpx.Timeout(3600.0, connect=30.0), + ) as response: + if response.status_code == 409: + lease_lost.set() + raise _LeaseLostError("Lease rejected during task input download") + if response.status_code >= 400: + await response.aread() + raise RemoteSyncUnavailableError( + f"协调服务器拒绝输入下载:{_safe_remote_error(response)}" + ) + content_length = response.headers.get("content-length") + if content_length: + try: + announced_size = int(content_length) + except ValueError as exc: + raise RemoteSyncUnavailableError("远程输入长度无效") from exc + if announced_size > MAX_UPLOAD_BYTES: + raise RemoteSyncUnavailableError("远程任务输入超过 10 GB 下载上限") + if announced_size != expected_size: + raise RemoteSyncUnavailableError("远程任务输入大小已发生变化") + + output = await asyncio.to_thread(destination.open, "xb") + written = 0 + try: + async for chunk in response.aiter_bytes(8 * 1024 * 1024): + if lease_lost.is_set(): + raise _LeaseLostError("Lease expired during task input download") + written += len(chunk) + if written > MAX_UPLOAD_BYTES or written > expected_size: + raise RemoteSyncUnavailableError( + "远程任务输入超过声明的下载上限" + ) + await asyncio.to_thread(output.write, chunk) + finally: + await asyncio.to_thread(output.close) + if written != expected_size: + destination.unlink(missing_ok=True) + raise RemoteSyncUnavailableError("远程任务输入下载不完整") + + def _claimed_task_for_local(self, task: Task, worker_id: str) -> Task: + local = task.model_copy(deep=True) + local.options = { + **local.options, + REMOTE_MIRROR_OPTION: True, + } + local.options.pop(PREFERRED_WORKER_OPTION, None) + local.assigned_executor = worker_id + local.result = None + local.error = None + local.completed_at = None + local.progress = 0.0 + local.status = TaskStatus.QUEUED + local.message = "已从协调服务器领取,等待本机处理..." + local.completed_steps = [] + return local + + async def _maintain_lease( + self, + client: httpx.AsyncClient, + config: _RemoteConfig, + worker_id: str, + task_id: UUID, + lease_token: str, + initial_expiry: Any, + lease_lost: asyncio.Event, + ) -> None: + expiry = _parse_lease_expiry(initial_expiry) + renew_every = min(300.0, max(30.0, _LEASE_SECONDS / 3)) + heartbeat_every = min(60.0, max(10.0, config.interval_sec)) + next_renew = time.monotonic() + renew_every + + while not lease_lost.is_set(): + try: + await self._heartbeat(client, worker_id) + if time.monotonic() >= next_renew: + response = await client.post( + ( + f"workers/{quote(worker_id, safe='')}/tasks/" + f"{task_id}/lease/renew" + ), + json={ + "lease_token": lease_token, + "lease_seconds": _LEASE_SECONDS, + }, + ) + if response.status_code == 409: + lease_lost.set() + return + response.raise_for_status() + payload = response.json() + expiry = _parse_lease_expiry(payload.get("lease_expires_at")) + next_renew = time.monotonic() + renew_every + except httpx.RequestError: + if datetime.now(timezone.utc) >= expiry: + lease_lost.set() + return + except httpx.HTTPStatusError as exc: + if exc.response.status_code in {404, 409}: + lease_lost.set() + return + if datetime.now(timezone.utc) >= expiry: + lease_lost.set() + return + try: + await asyncio.wait_for( + lease_lost.wait(), + timeout=heartbeat_every, + ) + except asyncio.TimeoutError: + pass + + async def _maintain_remote_mirror( + self, + client: httpx.AsyncClient, + config: _RemoteConfig, + lease_lost: asyncio.Event, + ) -> None: + while not lease_lost.is_set(): + try: + await self._mirror_remote_tasks(client, config) + except asyncio.CancelledError: + raise + except Exception as exc: + log_event( + logger, + logging.WARNING, + "remote_sync.active_task_mirror_failed", + error_type=type(exc).__name__, + ) + try: + await asyncio.wait_for( + lease_lost.wait(), + timeout=config.interval_sec, + ) + except asyncio.TimeoutError: + pass + + async def _wait_for_local_terminal( + self, + task_id: UUID, + lease_lost: asyncio.Event, + ) -> Task | None: + while not lease_lost.is_set(): + task = self._store_getter().get(task_id) + if task and task.status in _TERMINAL_STATUSES: + return task + await asyncio.sleep(1.0) + return None + + async def _upload_completed_task( + self, + client: httpx.AsyncClient, + config: _RemoteConfig, + worker_id: str, + task: Task, + lease_token: str, + lease_lost: asyncio.Event, + ) -> dict[str, Any]: + output_dir = self._local_archive_dir(task) + if output_dir is None: + await self._report_failure( + client, + config, + worker_id, + task.id, + lease_token, + "EXE 本地任务完成后没有生成可上传的归档目录", + lease_lost=lease_lost, + ) + return {} + + data_root = Path(self._settings_getter().data_root).resolve() + staging_dir = data_root / "_remote_sync_client" / f"upload-{task.id}-{uuid4().hex}" + zip_path = staging_dir / "archive.zip" + staging_dir.mkdir(parents=True, exist_ok=False) + try: + portable_result = rewrite_result_image_paths( + sanitize_portable_result(task.result), + output_dir, + relative=True, + ) + result_json = json.dumps( + portable_result, + ensure_ascii=False, + separators=(",", ":"), + ) + if len(result_json.encode("utf-8")) > MAX_PORTABLE_RESULT_JSON_BYTES: + raise ValueError("portable result metadata exceeds the 4 MB limit") + await asyncio.to_thread( + build_archive_zip, + output_dir, + zip_path, + include_media=config.include_media, + ) + while not lease_lost.is_set(): + try: + with zip_path.open("rb") as archive: + response = await client.post( + ( + f"workers/{quote(worker_id, safe='')}/tasks/" + f"{task.id}/archive" + ), + data={ + "lease_token": lease_token, + "result_json": result_json, + }, + files={"archive": ("archive.zip", archive, "application/zip")}, + timeout=httpx.Timeout(3600.0, connect=30.0), + ) + if response.status_code == 409: + lease_lost.set() + raise _LeaseLostError("Lease rejected during result upload") + response.raise_for_status() + payload = response.json() + log_event( + logger, + logging.INFO, + "remote_sync.archive_uploaded", + task_id=task.id, + worker_id=worker_id, + ) + return payload if isinstance(payload, dict) else {} + except httpx.RequestError: + await asyncio.sleep(config.interval_sec) + raise _LeaseLostError("Lease expired before result upload completed") + except (OSError, ValueError) as exc: + await self._report_failure( + client, + config, + worker_id, + task.id, + lease_token, + f"EXE 归档打包失败: {exc}", + lease_lost=lease_lost, + ) + return {} + finally: + await asyncio.to_thread(shutil.rmtree, staging_dir, True) + + async def _report_failure( + self, + client: httpx.AsyncClient, + config: _RemoteConfig, + worker_id: str, + task_id: UUID, + lease_token: str, + error: str, + *, + lease_lost: asyncio.Event | None = None, + ) -> None: + message = str(error or "EXE 处理失败")[:20_000] + while lease_lost is None or not lease_lost.is_set(): + try: + response = await client.post( + f"workers/{quote(worker_id, safe='')}/tasks/{task_id}/fail", + json={"lease_token": lease_token, "error": message}, + ) + if response.status_code == 409: + if lease_lost is not None: + lease_lost.set() + raise _LeaseLostError("Lease rejected while reporting failure") + response.raise_for_status() + return + except httpx.RequestError: + if lease_lost is None: + raise + await asyncio.sleep(config.interval_sec) + raise _LeaseLostError("Lease expired before failure report completed") + + def _apply_remote_revision( + self, + local_task: Task, + payload: dict[str, Any], + ) -> None: + remote_payload = payload.get("task") if isinstance(payload, dict) else None + if not isinstance(remote_payload, dict): + return + try: + revision = int(remote_payload.get("sync_revision") or 0) + except (TypeError, ValueError): + return + store = self._store_getter() + synced = store.get(local_task.id) or local_task.model_copy(deep=True) + synced.status = TaskStatus.COMPLETED + synced.message = "处理完成,已同步到服务器" + synced.sync_revision = max(0, revision) + synced.options = { + **synced.options, + REMOTE_MIRROR_OPTION: True, + REMOTE_ARCHIVE_REVISION_OPTION: max(0, revision), + } + store.save_remote_mirror(synced) + + def _local_archive_dir(self, task: Task) -> Path | None: + result = task.result if isinstance(task.result, dict) else {} + raw = result.get("output_dir") + if not raw: + return None + data_root = Path(self._settings_getter().data_root).resolve() + try: + output_dir = Path(str(raw)).resolve() + output_dir.relative_to(data_root) + except (OSError, ValueError): + return None + if not output_dir.is_dir() or not (output_dir / "metadata.json").is_file(): + return None + return output_dir + + def _cleanup_staged_source(self, task: Task) -> None: + if _is_http_source(task.source): + return + data_root = Path(self._settings_getter().data_root).resolve() + staging_root = (data_root / "_staging").resolve() + try: + source = Path(task.source).resolve() + source.relative_to(staging_root) + except (OSError, ValueError): + return + try: + source.unlink(missing_ok=True) + source.parent.rmdir() + except OSError: + pass + + async def _mirror_remote_tasks( + self, + client: httpx.AsyncClient, + config: _RemoteConfig, + ) -> None: + offset = 0 + while True: + response = await client.get( + "tasks", + params={"limit": _MIRROR_LIMIT, "offset": offset}, + ) + response.raise_for_status() + payload = response.json() + if not isinstance(payload, list): + raise RemoteSyncUnavailableError("协调服务器返回了无效的任务列表") + + for item in reversed(payload): + try: + remote_task = Task.model_validate(item) + except Exception: + continue + if remote_task.id == self._active_task_id: + continue + try: + await self._mirror_remote_task(client, config, remote_task) + except asyncio.CancelledError: + raise + except Exception as exc: + log_event( + logger, + logging.WARNING, + "remote_sync.task_mirror_failed", + task_id=remote_task.id, + error_type=type(exc).__name__, + ) + + if len(payload) < _MIRROR_LIMIT: + break + offset += len(payload) + + async def _mirror_remote_task( + self, + client: httpx.AsyncClient, + config: _RemoteConfig, + remote_task: Task, + ) -> None: + store = self._store_getter() + local_task = store.get(remote_task.id) + if local_task and not local_task.options.get(REMOTE_MIRROR_OPTION): + return + + local_archive = self._local_archive_dir(local_task) if local_task else None + archive_revision = self._local_archive_revision(local_task) + archive_is_current = ( + local_archive is not None + and archive_revision >= remote_task.sync_revision + ) + archive_refresh_required = ( + remote_task.status == TaskStatus.COMPLETED + and config.download_results + and not archive_is_current + ) + if ( + local_task is not None + and local_task.status == remote_task.status + and local_task.sync_revision >= remote_task.sync_revision + and not archive_refresh_required + ): + return + + files: dict[str, str] | None = None + if archive_refresh_required: + local_archive, files = await self._download_remote_archive( + client, + config, + remote_task, + ) + archive_revision = remote_task.sync_revision + mirrored = self._task_for_local_mirror( + remote_task, + local_archive=local_archive, + files=files, + archive_revision=( + archive_revision + if local_archive is not None and archive_revision >= 0 + else None + ), + server_url=config.api_base.removesuffix("/api/"), + ) + store.save_remote_mirror(mirrored) + + @staticmethod + def _local_archive_revision(task: Task | None) -> int: + if task is None: + return -1 + raw = task.options.get(REMOTE_ARCHIVE_REVISION_OPTION) + try: + return max(-1, int(raw)) + except (TypeError, ValueError): + return -1 + + async def _download_remote_archive( + self, + client: httpx.AsyncClient, + config: _RemoteConfig, + task: Task, + ) -> tuple[Path, dict[str, str]]: + data_root = Path(self._settings_getter().data_root).resolve() + staging_dir = data_root / "_remote_sync_client" / f"download-{task.id}-{uuid4().hex}" + zip_path = staging_dir / "archive.zip" + extraction_dir = staging_dir / "extracted" + destination = data_root / f"remote-{task.id}" + staging_dir.mkdir(parents=True, exist_ok=False) + try: + await self._stream_archive_download( + client, + task.id, + zip_path, + include_media=config.include_media, + ) + extracted_root = await asyncio.to_thread( + safe_extract_zip, + zip_path, + extraction_dir, + ) + await asyncio.to_thread( + publish_archive, + extracted_root, + destination, + data_root, + ) + await asyncio.to_thread(rewrite_archive_image_paths, destination) + files = await asyncio.to_thread(archive_file_manifest, destination) + log_event( + logger, + logging.INFO, + "remote_sync.archive_downloaded", + task_id=task.id, + ) + return destination, files + finally: + await asyncio.to_thread(shutil.rmtree, staging_dir, True) + + async def _stream_archive_download( + self, + client: httpx.AsyncClient, + task_id: UUID, + destination: Path, + *, + include_media: bool, + ) -> None: + async with client.stream( + "GET", + f"sync/tasks/{task_id}/archive", + params={"include_media": str(include_media).lower()}, + timeout=httpx.Timeout(3600.0, connect=30.0), + ) as response: + response.raise_for_status() + content_length = response.headers.get("content-length") + if content_length: + try: + if int(content_length) > MAX_UPLOAD_BYTES: + raise RemoteSyncUnavailableError("远程归档超过 10 GB 下载上限") + except ValueError: + pass + + output = await asyncio.to_thread(destination.open, "wb") + written = 0 + try: + async for chunk in response.aiter_bytes(8 * 1024 * 1024): + written += len(chunk) + if written > MAX_UPLOAD_BYTES: + raise RemoteSyncUnavailableError("远程归档超过 10 GB 下载上限") + await asyncio.to_thread(output.write, chunk) + except Exception: + destination.unlink(missing_ok=True) + raise + finally: + await asyncio.to_thread(output.close) + + def _task_for_local_mirror( + self, + task: Task, + *, + local_archive: Path | None, + files: dict[str, str] | None, + archive_revision: int | None = None, + server_url: str, + ) -> Task: + mirrored = task.model_copy(deep=True) + mirrored.options = { + **mirrored.options, + REMOTE_MIRROR_OPTION: True, + } + if archive_revision is not None: + mirrored.options[REMOTE_ARCHIVE_REVISION_OPTION] = archive_revision + remote_result = dict(mirrored.result or {}) + remote_output_dir = remote_result.pop("output_dir", None) + archive = remote_result.get("archive") + if isinstance(archive, dict): + archive = dict(archive) + archive.pop("output_dir", None) + archive.pop("files", None) + if archive: + remote_result["archive"] = archive + else: + remote_result.pop("archive", None) + sync_info = ( + dict(remote_result.get("remote_sync") or {}) + if isinstance(remote_result.get("remote_sync"), dict) + else {} + ) + sync_info.update( + { + "mirror": True, + "server_url": server_url, + "remote_output_dir": remote_output_dir, + "sync_revision": mirrored.sync_revision, + } + ) + remote_result["remote_sync"] = sync_info + if local_archive is not None: + local_files = files or archive_file_manifest(local_archive) + remote_result["output_dir"] = str(local_archive) + remote_result["archive"] = { + "output_dir": str(local_archive), + "files": local_files, + } + remote_result = rewrite_result_image_paths( + remote_result, + local_archive, + ) + mirrored.result = remote_result + return mirrored + + async def forward_task(self, task_create: TaskCreate) -> Task: + """Create one URL task on the coordinator and retain a safe local mirror.""" + settings = self._settings_getter() + if not settings.remote_sync_enabled: + raise RemoteSyncConfigurationError("远程协调尚未启用") + config = _RemoteConfig.from_settings(settings) + requested_executor = ( + task_create.requested_executor + if "requested_executor" in task_create.model_fields_set + else settings.default_task_executor + ) + if requested_executor == "exe" and not config.upload_results: + raise RemoteSyncConfigurationError( + "EXE 结果上传已关闭,无法创建由 EXE 处理的远程任务" + ) + is_http_source = _is_http_source(task_create.source) + if not is_http_source: + if requested_executor != "exe": + raise RemoteSyncConfigurationError( + "本地文件只能指派给 EXE;服务器无法访问 EXE 本地路径" + ) + if task_create.source.startswith("upload://"): + raise RemoteSyncConfigurationError( + "浏览器暂存文件缺少稳定路径,请重新选择本地文件" + ) + source_path = Path(task_create.source) + if not source_path.is_file(): + raise RemoteSyncConfigurationError("本地任务来源必须是当前 EXE 可读取的文件") + + payload = task_create.model_dump(mode="json") + payload.update( + { + "origin_client": "exe", + "requested_executor": requested_executor, + } + ) + + try: + async with self._make_client(config) as client: + worker_id = await self._ensure_registered(client, config) + if not is_http_source: + payload["options"] = { + **payload.get("options", {}), + PREFERRED_WORKER_OPTION: worker_id, + } + response = await client.post("tasks", json=payload) + if response.status_code >= 400: + raise RemoteSyncUnavailableError( + f"协调服务器拒绝了任务:{_safe_remote_error(response)}" + ) + remote_task = Task.model_validate(response.json()) + except httpx.RequestError as exc: + raise RemoteSyncUnavailableError("无法连接远程协调服务器") from exc + + mirrored = self._task_for_local_mirror( + remote_task, + local_archive=None, + files=None, + archive_revision=None, + server_url=config.api_base.removesuffix("/api/"), + ) + self._store_getter().save_remote_mirror(mirrored) + return mirrored + + +_remote_sync_service: RemoteSyncService | None = None + + +def get_remote_sync_service() -> RemoteSyncService: + global _remote_sync_service + if _remote_sync_service is None: + _remote_sync_service = RemoteSyncService() + return _remote_sync_service + + +def reset_remote_sync_service() -> None: + global _remote_sync_service + _remote_sync_service = None diff --git a/pyproject.toml b/pyproject.toml index 2b2a8b7..855b51f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ dependencies = [ "transformers>=4.45.0", "python-multipart>=0.0.21", "numpy>=1.26.0", + "pillow>=12.1.0", # CLI "typer>=0.15.0", "rich>=13.0.0", diff --git a/tests/test_backend_api_smoke.py b/tests/test_backend_api_smoke.py index 96655f8..dbc97fd 100644 --- a/tests/test_backend_api_smoke.py +++ b/tests/test_backend_api_smoke.py @@ -20,6 +20,7 @@ from app.core.events import EventBus # noqa: E402 from app.core.settings import RuntimeSettings # noqa: E402 from app.models import Task, TaskStatus, TaskType # noqa: E402 +from app.services import analysis as analysis_service # noqa: E402 from app.services.ingestion import ytdlp # noqa: E402 from app.services.ingestion.platform.bilibili import auth as bilibili_auth # noqa: E402 from app.services.ingestion.platform.bilibili import collection as bilibili_collection # noqa: E402 @@ -126,6 +127,66 @@ def test_backend_api_smoke_triggers_core_boundaries(tmp_path, monkeypatch): assert delete_staged.json() == {"deleted": True} +def test_archive_extra_polish_is_persisted(tmp_path, monkeypatch): + client, _queue = _client(tmp_path, monkeypatch) + archive_dir = tmp_path / "archive" + archive_dir.mkdir() + (archive_dir / "metadata.json").write_text( + json.dumps({"title": "演示任务"}, ensure_ascii=False), + encoding="utf-8", + ) + (archive_dir / "transcript_polished.md").write_text("初次润色内容", encoding="utf-8") + task = Task( + task_type=TaskType.PIPELINE, + status=TaskStatus.COMPLETED, + source="https://example.com/video", + result={ + "output_dir": str(archive_dir), + "metadata": {"title": "演示任务"}, + }, + sync_revision=2, + ) + database.get_task_store().save(task) + + async def fake_polish(text, context=None): + assert text == "初次润色内容" + assert context == {"title": "演示任务", "additional_pass": True} + return "额外润色内容" + + monkeypatch.setattr(analysis_service, "polish_text", fake_polish) + response = client.post( + "/api/pipeline/archives/polish", + json={"path": str(archive_dir)}, + ) + + assert response.status_code == 200 + assert response.json()["filename"] == "transcript_extra_polished.md" + assert ( + archive_dir / "transcript_extra_polished.md" + ).read_text(encoding="utf-8") == "额外润色内容" + metadata = json.loads((archive_dir / "metadata.json").read_text(encoding="utf-8")) + assert metadata["extra_polish_file"] == "transcript_extra_polished.md" + assert metadata["extra_polish_source"] == "transcript_polished.md" + saved = database.get_task_store().get(task.id) + assert response.json()["sync_revision"] == 3 + assert saved.sync_revision == 3 + assert saved.result["extra_polish_file"] == "transcript_extra_polished.md" + + +def test_archive_extra_polish_rejects_outside_data_root(tmp_path, monkeypatch): + client, _queue = _client(tmp_path, monkeypatch) + outside = tmp_path.parent / f"{tmp_path.name}-outside-polish" + outside.mkdir(exist_ok=True) + try: + response = client.post( + "/api/pipeline/archives/polish", + json={"path": str(outside), "text": "内容"}, + ) + assert response.status_code == 403 + finally: + outside.rmdir() + + def test_batch_task_creation_uses_shared_options(tmp_path, monkeypatch): client, queue = _client(tmp_path, monkeypatch) @@ -433,6 +494,123 @@ def test_archives_lite_omits_heavy_metadata_and_detail_returns_full(tmp_path, mo assert full["analysis"]["keywords"] == ["heavy"] +def test_archive_delete_only_removes_registered_terminal_archive(tmp_path, monkeypatch): + client, _queue = _client(tmp_path, monkeypatch) + internal_dir = tmp_path / "_remote_sync" + internal_dir.mkdir() + unregistered_dir = tmp_path / "unregistered" + unregistered_dir.mkdir() + + assert client.request( + "DELETE", + "/api/pipeline/archives", + json={"path": str(tmp_path)}, + ).status_code == 403 + assert client.request( + "DELETE", + "/api/pipeline/archives", + json={"path": str(internal_dir)}, + ).status_code == 403 + assert client.request( + "DELETE", + "/api/pipeline/archives", + json={"path": str(unregistered_dir)}, + ).status_code == 404 + + archive_dir = tmp_path / "registered" + archive_dir.mkdir() + (archive_dir / "metadata.json").write_text( + json.dumps({"title": "Registered"}), + encoding="utf-8", + ) + task = Task( + task_type=TaskType.PIPELINE, + status=TaskStatus.COMPLETED, + source="https://example.com/registered", + result={"output_dir": str(archive_dir)}, + ) + store = database.get_task_store() + store.save(task) + + response = client.request( + "DELETE", + "/api/pipeline/archives", + json={"path": str(archive_dir)}, + ) + + assert response.status_code == 200 + assert response.json()["task_deleted"] is True + assert not archive_dir.exists() + assert store.get(task.id) is None + + +def test_remote_mirror_extra_polish_is_applied_on_coordinator(tmp_path, monkeypatch): + client, _queue = _client(tmp_path, monkeypatch) + settings = pipeline.get_runtime_settings() + settings.remote_sync_enabled = True + settings.remote_server_url = "https://coordinator.example" + settings.remote_api_token = "worker-secret" + + archive_dir = tmp_path / "remote-task" + archive_dir.mkdir() + (archive_dir / "metadata.json").write_text( + json.dumps({"title": "Remote"}), + encoding="utf-8", + ) + task = Task( + task_type=TaskType.PIPELINE, + status=TaskStatus.COMPLETED, + source="https://example.com/remote", + options={"_mpp_remote_mirror": True}, + result={ + "output_dir": str(archive_dir), + "remote_sync": { + "mirror": True, + "remote_output_dir": "/srv/mpp/remote-task", + }, + }, + ) + database.get_task_store().save_remote_mirror(task) + requests: list[tuple[str, dict, dict]] = [] + + class FakeResponse: + status_code = 200 + + @staticmethod + def json(): + return { + "polished": "remote polished", + "filename": "transcript_extra_polished.md", + "sync_revision": 9, + } + + class FakeClient: + async def __aenter__(self): + return self + + async def __aexit__(self, *_args): + return None + + async def post(self, url, *, json, headers): + requests.append((url, json, headers)) + return FakeResponse() + + monkeypatch.setattr(pipeline.httpx, "AsyncClient", lambda **_kwargs: FakeClient()) + + response = client.post( + "/api/pipeline/archives/polish", + json={"path": str(archive_dir), "text": "local input"}, + ) + + assert response.status_code == 200 + assert response.json()["polished"] == "remote polished" + assert requests[0][0].endswith("/api/pipeline/archives/polish") + assert requests[0][1]["path"] == "/srv/mpp/remote-task" + assert requests[0][1]["text"] == "local input" + assert requests[0][2]["Authorization"] == "Bearer worker-secret" + assert not (archive_dir / "transcript_extra_polished.md").exists() + + def test_bilibili_opus_note_cover_thumbnail_is_cached_in_metadata(tmp_path): Image = pytest.importorskip("PIL.Image") from app.core.pipeline import _cache_note_cover_thumbnail @@ -513,6 +691,19 @@ async def stop(self) -> None: assert queue.stopped is True +def test_main_app_required_api_token_fails_closed(tmp_path, monkeypatch): + from app import main as app_main + + settings = RuntimeSettings(data_root=str(tmp_path), api_token="") + monkeypatch.setattr(settings_module, "_runtime_settings", settings) + monkeypatch.setattr(app_main, "get_runtime_settings", lambda: settings) + monkeypatch.setenv("MPP_REQUIRE_API_TOKEN", "1") + + with pytest.raises(RuntimeError, match="api_token is empty or unavailable"): + with TestClient(app_main.app): + pass + + def test_main_app_lifespan_auto_updates_ytdlp_when_enabled(tmp_path, monkeypatch): from app import main as app_main from app.services.ingestion import ytdlp_version diff --git a/tests/test_model_router_multimodal_kb.py b/tests/test_model_router_multimodal_kb.py index 736a94d..5cd8a5d 100644 --- a/tests/test_model_router_multimodal_kb.py +++ b/tests/test_model_router_multimodal_kb.py @@ -41,7 +41,7 @@ def test_vlm_binding_uses_siliconflow_defaults_when_vlm_endpoint_is_empty(): binding = resolve_vlm_binding(settings) assert binding.configured is True - assert binding.model == "Qwen/Qwen3.5-4B" + assert binding.model == "Qwen/Qwen3-VL-8B-Instruct" assert binding.api_base == "https://api.siliconflow.cn/v1" assert binding.api_key == "sf-key" diff --git a/tests/test_pipeline_model_bindings.py b/tests/test_pipeline_model_bindings.py index 2c5296b..b4be9a4 100644 --- a/tests/test_pipeline_model_bindings.py +++ b/tests/test_pipeline_model_bindings.py @@ -69,7 +69,7 @@ def test_pipeline_binding_for_image_note_uses_vlm_when_images_are_present(): assert binding.run_polish is False assert binding.run_analysis is True assert binding.run_vlm is True - assert binding.vlm.model == "Qwen/Qwen3.5-4B" + assert binding.vlm.model == "Qwen/Qwen3-VL-8B-Instruct" def test_pipeline_binding_for_image_note_uses_siliconflow_vlm_fallback(): @@ -88,7 +88,7 @@ def test_pipeline_binding_for_image_note_uses_siliconflow_vlm_fallback(): ) assert binding.run_vlm is True - assert binding.vlm.model == "Qwen/Qwen3.5-4B" + assert binding.vlm.model == "Qwen/Qwen3-VL-8B-Instruct" assert binding.vlm.api_base == "https://api.siliconflow.cn/v1" diff --git a/tests/test_remote_sync.py b/tests/test_remote_sync.py new file mode 100644 index 0000000..1263597 --- /dev/null +++ b/tests/test_remote_sync.py @@ -0,0 +1,954 @@ +from __future__ import annotations + +import io +import json +import os +import sqlite3 +import sys +import zipfile +from concurrent.futures import ThreadPoolExecutor +from datetime import datetime, timedelta, timezone +from pathlib import Path +from uuid import UUID, uuid4 + +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient + +ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(ROOT / "backend")) + +from app.api.routes import pipeline, sync, tasks # noqa: E402 +from app.core import archive_sync, database # noqa: E402 +from app.core import settings as settings_module # noqa: E402 +from app.core.archive_sync import safe_extract_zip # noqa: E402 +from app.core.queue import TaskQueue # noqa: E402 +from app.core.settings import RuntimeSettings # noqa: E402 +from app.models import Task, TaskStatus, TaskType # noqa: E402 +from app.models.task import PREFERRED_WORKER_OPTION # noqa: E402 + + +class FakeQueue: + def __init__(self) -> None: + self.submitted: list[UUID] = [] + + async def submit(self, task_id: UUID) -> None: + self.submitted.append(task_id) + + +@pytest.fixture +def sync_client(tmp_path: Path, monkeypatch): + runtime = RuntimeSettings(data_root=str(tmp_path), api_token="") + monkeypatch.setattr(settings_module, "_runtime_settings", runtime) + monkeypatch.setattr(settings_module, "_save_settings_to_file", lambda _settings: None) + database.reset_db_path(tmp_path) + fake_queue = FakeQueue() + monkeypatch.setattr(tasks, "get_task_queue", lambda: fake_queue) + + app = FastAPI() + app.include_router(tasks.router, prefix="/api") + app.include_router(sync.router, prefix="/api") + with TestClient(app) as client: + yield client, fake_queue, tmp_path + database.reset_db_path() + + +def _task( + *, + executor: str = "exe", + status: TaskStatus = TaskStatus.QUEUED, + result: dict | None = None, +) -> Task: + return Task( + id=uuid4(), + task_type=TaskType.PIPELINE, + status=status, + source="https://example.com/media", + requested_executor=executor, + assigned_executor="server" if executor == "server" else None, + origin_client="test", + result=result, + ) + + +def _zip_bytes(files: dict[str, bytes | str]) -> bytes: + output = io.BytesIO() + with zipfile.ZipFile(output, "w", compression=zipfile.ZIP_DEFLATED) as archive: + for name, content in files.items(): + archive.writestr(name, content.encode("utf-8") if isinstance(content, str) else content) + return output.getvalue() + + +def _register_and_claim(client: TestClient, worker_id: str = "exe-test") -> dict: + registered = client.post( + "/api/workers/register", + json={ + "worker_id": worker_id, + "name": "Test EXE", + "capabilities": {"gpu": True}, + }, + ) + assert registered.status_code == 200 + claimed = client.post( + f"/api/workers/{worker_id}/claim", + json={"lease_seconds": 900}, + ) + assert claimed.status_code == 200 + assert claimed.json()["task"] is not None + return claimed.json() + + +def _lease_state(task_id: UUID | str) -> str | None: + row = database._get_conn().execute( + "SELECT state FROM task_leases WHERE task_id = ?", + (str(task_id),), + ).fetchone() + return str(row["state"]) if row else None + + +def test_existing_database_is_migrated_with_sync_defaults(tmp_path): + db_path = tmp_path / "tasks.db" + conn = sqlite3.connect(db_path) + conn.executescript( + """ + CREATE TABLE tasks ( + id TEXT PRIMARY KEY, + task_type TEXT NOT NULL, + status TEXT NOT NULL, + source TEXT NOT NULL, + options TEXT NOT NULL DEFAULT '{}', + progress REAL NOT NULL DEFAULT 0, + message TEXT, + result TEXT, + error TEXT, + webhook_url TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + completed_at TEXT, + current_step TEXT, + steps TEXT NOT NULL DEFAULT '[]', + completed_steps TEXT NOT NULL DEFAULT '[]', + flow TEXT, + platform TEXT, + uploader_id TEXT, + content_subtype TEXT + ); + """ + ) + task_id = str(uuid4()) + now = datetime.now().isoformat() + conn.execute( + """ + INSERT INTO tasks + (id, task_type, status, source, options, created_at, updated_at, steps, completed_steps) + VALUES (?, 'pipeline', 'completed', 'legacy.mp4', '{}', ?, ?, '[]', '[]') + """, + (task_id, now, now), + ) + conn.commit() + conn.close() + + database.reset_db_path(tmp_path) + task = database.get_task_store().get(UUID(task_id)) + assert task is not None + assert task.origin_client == "legacy" + assert task.requested_executor == "server" + assert task.assigned_executor is None + assert task.sync_revision == 0 + + columns = { + row["name"] + for row in database._get_conn().execute("PRAGMA table_info(tasks)").fetchall() + } + assert {"origin_client", "requested_executor", "assigned_executor", "sync_revision"} <= columns + assert database._get_conn().execute( + "SELECT name FROM sqlite_master WHERE type='table' AND name='task_leases'" + ).fetchone() + database.reset_db_path() + + +def test_task_api_accepts_origin_and_requested_executor(sync_client): + client, queue, _tmp_path = sync_client + response = client.post( + "/api/tasks", + json={ + "task_type": "pipeline", + "source": "https://example.com/video", + "origin_client": "apk", + "requested_executor": "exe", + }, + ) + assert response.status_code == 200 + payload = response.json() + assert payload["origin_client"] == "apk" + assert payload["requested_executor"] == "exe" + assert payload["assigned_executor"] is None + assert payload["message"] == "等待 EXE 处理..." + assert queue.submitted == [UUID(payload["id"])] + + +@pytest.mark.asyncio +async def test_local_queue_only_enqueues_server_tasks(tmp_path, monkeypatch): + runtime = RuntimeSettings(data_root=str(tmp_path), max_download_concurrency=1) + monkeypatch.setattr(settings_module, "_runtime_settings", runtime) + database.reset_db_path(tmp_path) + store = database.get_task_store() + server_task = _task(executor="server") + exe_task = _task(executor="exe") + store.save(server_task) + store.save(exe_task) + + queue = TaskQueue() + await queue.submit(server_task.id) + await queue.submit(exe_task.id) + + assert queue.get_queue_snapshot() == [server_task.id] + saved_exe = store.get(exe_task.id) + assert saved_exe is not None + assert saved_exe.status == TaskStatus.QUEUED + assert saved_exe.message == "等待 EXE 处理..." + database.reset_db_path() + + +@pytest.mark.asyncio +async def test_local_queue_preserves_preferred_exe_assignment(tmp_path, monkeypatch): + runtime = RuntimeSettings(data_root=str(tmp_path), max_download_concurrency=1) + monkeypatch.setattr(settings_module, "_runtime_settings", runtime) + database.reset_db_path(tmp_path) + task = _task() + task.assigned_executor = "desktop-a" + task.options[PREFERRED_WORKER_OPTION] = "desktop-a" + database.get_task_store().save(task) + + queue = TaskQueue() + await queue.submit(task.id) + + saved = database.get_task_store().get(task.id) + assert saved is not None + assert saved.assigned_executor == "desktop-a" + assert queue.get_queue_snapshot() == [] + database.reset_db_path() + + +def test_workers_list_includes_server_and_online_exe(sync_client): + client, _queue, _tmp_path = sync_client + client.post( + "/api/workers/register", + json={"worker_id": "desktop-a", "name": "Desktop A", "capabilities": {"vram_gb": 16}}, + ) + response = client.get("/api/workers") + assert response.status_code == 200 + workers = response.json()["workers"] + assert workers[0]["id"] == "server" + assert workers[0]["online"] is True + desktop = next(worker for worker in workers if worker["id"] == "desktop-a") + assert desktop["executor"] == "exe" + assert desktop["online"] is True + assert desktop["capabilities"]["vram_gb"] == 16 + + +def test_claimed_worker_downloads_only_its_server_staged_input(sync_client): + client, _queue, tmp_path = sync_client + staged_dir = tmp_path / "_staging" / uuid4().hex + staged_dir.mkdir(parents=True) + media = staged_dir / "手机 上传.mp4" + media.write_bytes(b"server-staged-media") + task = _task() + task.source = str(media) + database.get_task_store().save(task) + + claim = _register_and_claim(client) + + assert claim["task"]["id"] == str(task.id) + assert claim["input"] == { + "filename": media.name, + "size": len(b"server-staged-media"), + } + missing_token = client.get( + f"/api/workers/exe-test/tasks/{task.id}/input", + ) + assert missing_token.status_code == 422 + wrong_token = client.get( + f"/api/workers/exe-test/tasks/{task.id}/input", + headers={"X-MPP-Lease-Token": "another-lease-token-123"}, + ) + assert wrong_token.status_code == 409 + + downloaded = client.get( + f"/api/workers/exe-test/tasks/{task.id}/input", + headers={"X-MPP-Lease-Token": claim["lease_token"]}, + ) + assert downloaded.status_code == 200 + assert downloaded.content == b"server-staged-media" + assert downloaded.headers["cache-control"] == "private, no-store" + assert downloaded.headers["x-mpp-input-size"] == str(len(downloaded.content)) + + +def test_claimed_input_rejects_files_outside_staging_and_over_limit( + sync_client, + monkeypatch, +): + client, _queue, tmp_path = sync_client + outside = tmp_path / "private.mp4" + outside.write_bytes(b"private") + outside_task = _task() + outside_task.source = str(outside) + database.get_task_store().save(outside_task) + outside_claim = _register_and_claim(client) + assert "input" not in outside_claim + outside_download = client.get( + f"/api/workers/exe-test/tasks/{outside_task.id}/input", + headers={"X-MPP-Lease-Token": outside_claim["lease_token"]}, + ) + assert outside_download.status_code == 403 + + database.get_task_store().fail_remote_task( + outside_task.id, + "exe-test", + outside_claim["lease_token"], + "test cleanup", + ) + staged_dir = tmp_path / "_staging" / uuid4().hex + staged_dir.mkdir(parents=True) + oversized = staged_dir / "oversized.mp4" + oversized.write_bytes(b"12345") + oversized_task = _task() + oversized_task.source = str(oversized) + database.get_task_store().save(oversized_task) + monkeypatch.setattr(sync, "MAX_UPLOAD_BYTES", 4) + oversized_claim = _register_and_claim(client) + assert "input" not in oversized_claim + oversized_download = client.get( + f"/api/workers/exe-test/tasks/{oversized_task.id}/input", + headers={"X-MPP-Lease-Token": oversized_claim["lease_token"]}, + ) + assert oversized_download.status_code == 413 + + +def test_staging_sweep_retains_input_for_offline_exe_task( + sync_client, +): + _client, _queue, tmp_path = sync_client + staging_root = tmp_path / "_staging" + active_dir = staging_root / uuid4().hex + stale_dir = staging_root / uuid4().hex + active_dir.mkdir(parents=True) + stale_dir.mkdir() + active_source = active_dir / "waiting.mp4" + active_source.write_bytes(b"waiting") + (stale_dir / "abandoned.mp4").write_bytes(b"abandoned") + os.utime(active_dir, (1, 1)) + os.utime(stale_dir, (1, 1)) + + task = _task() + task.source = str(active_source) + database.get_task_store().save(task) + + removed = pipeline.sweep_stale_staging(max_age_hours=0) + + assert removed == 1 + assert active_source.is_file() + assert not stale_dir.exists() + + +def test_concurrent_claim_has_one_winner(tmp_path, monkeypatch): + runtime = RuntimeSettings(data_root=str(tmp_path)) + monkeypatch.setattr(settings_module, "_runtime_settings", runtime) + database.reset_db_path(tmp_path) + store = database.get_task_store() + task = _task() + store.save(task) + store.register_worker(worker_id="exe-a", name="A") + store.register_worker(worker_id="exe-b", name="B") + + with ThreadPoolExecutor(max_workers=2) as pool: + results = list( + pool.map( + lambda worker_id: store.claim_remote_task(worker_id), + ["exe-a", "exe-b"], + ) + ) + + winners = [result for result in results if result is not None] + assert len(winners) == 1 + claimed = store.get(task.id) + assert claimed is not None + assert claimed.status == TaskStatus.PROCESSING + assert claimed.assigned_executor in {"exe-a", "exe-b"} + database.reset_db_path() + + +def test_concurrent_upload_reservation_has_one_winner(tmp_path, monkeypatch): + runtime = RuntimeSettings(data_root=str(tmp_path)) + monkeypatch.setattr(settings_module, "_runtime_settings", runtime) + database.reset_db_path(tmp_path) + store = database.get_task_store() + task = _task() + store.save(task) + store.register_worker(worker_id="exe-a", name="A") + claim = store.claim_remote_task("exe-a") + assert claim is not None + + with ThreadPoolExecutor(max_workers=2) as pool: + results = list( + pool.map( + lambda _index: store.begin_remote_upload( + task.id, + "exe-a", + claim["lease_token"], + ), + range(2), + ) + ) + + winners = [result for result in results if result is not None] + assert len(winners) == 1 + assert winners[0]["state"] == "uploading" + assert _lease_state(task.id) == "uploading" + assert store.release_remote_upload(task.id, "exe-a", claim["lease_token"]) is True + assert _lease_state(task.id) == "active" + database.reset_db_path() + + +def test_preferred_worker_is_the_only_worker_allowed_to_claim(sync_client): + client, _queue, _tmp_path = sync_client + created = client.post( + "/api/tasks", + json={ + "task_type": "pipeline", + "source": "https://example.com/video", + "requested_executor": "exe", + "options": {PREFERRED_WORKER_OPTION: "desktop-a"}, + }, + ) + assert created.status_code == 200 + assert created.json()["assigned_executor"] == "desktop-a" + + client.post( + "/api/workers/register", + json={"worker_id": "desktop-b", "name": "Desktop B"}, + ) + rejected = client.post( + "/api/workers/desktop-b/claim", + json={"lease_seconds": 900}, + ) + assert rejected.status_code == 200 + assert rejected.json()["task"] is None + + client.post( + "/api/workers/register", + json={"worker_id": "desktop-a", "name": "Desktop A"}, + ) + claimed = client.post( + "/api/workers/desktop-a/claim", + json={"lease_seconds": 900}, + ) + assert claimed.status_code == 200 + assert claimed.json()["task"]["id"] == created.json()["id"] + + +def test_preferred_worker_survives_lease_expiry(tmp_path, monkeypatch): + runtime = RuntimeSettings(data_root=str(tmp_path)) + monkeypatch.setattr(settings_module, "_runtime_settings", runtime) + database.reset_db_path(tmp_path) + store = database.get_task_store() + task = _task() + task.options[PREFERRED_WORKER_OPTION] = "exe-a" + task.assigned_executor = "exe-a" + store.save(task) + store.register_worker(worker_id="exe-a", name="A") + store.register_worker(worker_id="exe-b", name="B") + first = store.claim_remote_task("exe-a") + assert first is not None + + expired = (datetime.now(timezone.utc) - timedelta(seconds=1)).isoformat() + database._get_conn().execute( + "UPDATE task_leases SET lease_expires_at = ? WHERE task_id = ?", + (expired, str(task.id)), + ) + database._get_conn().commit() + + assert store.claim_remote_task("exe-b") is None + second = store.claim_remote_task("exe-a") + assert second is not None + assert second["attempt"] == 2 + database.reset_db_path() + + +def test_expired_lease_can_be_reclaimed(tmp_path, monkeypatch): + runtime = RuntimeSettings(data_root=str(tmp_path)) + monkeypatch.setattr(settings_module, "_runtime_settings", runtime) + database.reset_db_path(tmp_path) + store = database.get_task_store() + task = _task() + store.save(task) + store.register_worker(worker_id="exe-a", name="A") + store.register_worker(worker_id="exe-b", name="B") + first = store.claim_remote_task("exe-a") + assert first is not None + + expired = (datetime.now(timezone.utc) - timedelta(seconds=1)).isoformat() + database._get_conn().execute( + "UPDATE task_leases SET lease_expires_at = ? WHERE task_id = ?", + (expired, str(task.id)), + ) + database._get_conn().commit() + + second = store.claim_remote_task("exe-b") + assert second is not None + assert second["task"].assigned_executor == "exe-b" + assert second["attempt"] == 2 + assert store.get_remote_lease(task.id, "exe-a", first["lease_token"]) is None + database.reset_db_path() + + +def test_repeated_worker_crashes_move_task_to_failed_and_unblock_queue( + tmp_path, + monkeypatch, +): + runtime = RuntimeSettings(data_root=str(tmp_path)) + monkeypatch.setattr(settings_module, "_runtime_settings", runtime) + database.reset_db_path(tmp_path) + store = database.get_task_store() + poison = _task() + following = _task() + following.created_at = poison.created_at + timedelta(seconds=1) + store.save(poison) + store.save(following) + store.register_worker(worker_id="exe-a", name="A") + + for expected_attempt in (1, 2, 3): + claim = store.claim_remote_task("exe-a") + assert claim is not None + assert claim["task"].id == poison.id + assert claim["attempt"] == expected_attempt + expired = (datetime.now(timezone.utc) - timedelta(seconds=1)).isoformat() + database._get_conn().execute( + "UPDATE task_leases SET lease_expires_at = ? WHERE task_id = ?", + (expired, str(poison.id)), + ) + database._get_conn().commit() + + next_claim = store.claim_remote_task("exe-a") + + assert next_claim is not None + assert next_claim["task"].id == following.id + failed = store.get(poison.id) + assert failed is not None + assert failed.status == TaskStatus.FAILED + assert "多次中断" in failed.message + database.reset_db_path() + + +@pytest.mark.parametrize("lease_phase", ["uploading", "finalizing"]) +def test_expired_upload_phase_can_be_reclaimed(tmp_path, monkeypatch, lease_phase): + runtime = RuntimeSettings(data_root=str(tmp_path)) + monkeypatch.setattr(settings_module, "_runtime_settings", runtime) + database.reset_db_path(tmp_path) + store = database.get_task_store() + task = _task() + store.save(task) + store.register_worker(worker_id="exe-a", name="A") + store.register_worker(worker_id="exe-b", name="B") + first = store.claim_remote_task("exe-a") + assert first is not None + assert store.begin_remote_upload( + task.id, + "exe-a", + first["lease_token"], + ) + if lease_phase == "finalizing": + assert store.begin_remote_finalization( + task.id, + "exe-a", + first["lease_token"], + ) + + expired = (datetime.now(timezone.utc) - timedelta(seconds=1)).isoformat() + database._get_conn().execute( + "UPDATE task_leases SET lease_expires_at = ? WHERE task_id = ?", + (expired, str(task.id)), + ) + database._get_conn().commit() + + second = store.claim_remote_task("exe-b") + assert second is not None + assert second["attempt"] == 2 + assert second["task"].assigned_executor == "exe-b" + assert store.get_remote_lease(task.id, "exe-a", first["lease_token"]) is None + database.reset_db_path() + + +@pytest.mark.parametrize( + ("control", "lease_phase", "expected_status", "expected_lease_state"), + [ + ("cancel", "active", "cancelled", "cancelled"), + ("cancel", "uploading", "cancelled", "cancelled"), + ("pause", "active", "paused", "paused"), + ("pause", "uploading", "paused", "paused"), + ], +) +def test_cancel_or_pause_terminates_mutable_remote_lease( + sync_client, + monkeypatch, + control, + lease_phase, + expected_status, + expected_lease_state, +): + client, _queue, _tmp_path = sync_client + created = client.post( + "/api/tasks", + json={ + "task_type": "pipeline", + "source": "https://example.com/video", + "requested_executor": "exe", + }, + ).json() + claim = _register_and_claim(client) + if lease_phase == "uploading": + assert database.get_task_store().begin_remote_upload( + UUID(created["id"]), + "exe-test", + claim["lease_token"], + ) + + control_queue = TaskQueue() + monkeypatch.setattr(tasks, "get_task_queue", lambda: control_queue) + response = client.post(f"/api/tasks/{created['id']}/{control}") + assert response.status_code == 200, response.text + assert client.get(f"/api/tasks/{created['id']}").json()["status"] == expected_status + assert _lease_state(created["id"]) == expected_lease_state + + renewed = client.post( + f"/api/workers/exe-test/tasks/{created['id']}/lease/renew", + json={"lease_token": claim["lease_token"], "lease_seconds": 900}, + ) + assert renewed.status_code == 409 + + +def test_lease_renewal_requires_processing_task(tmp_path, monkeypatch): + runtime = RuntimeSettings(data_root=str(tmp_path)) + monkeypatch.setattr(settings_module, "_runtime_settings", runtime) + database.reset_db_path(tmp_path) + store = database.get_task_store() + task = _task() + store.save(task) + store.register_worker(worker_id="exe-a", name="A") + claim = store.claim_remote_task("exe-a") + assert claim is not None + + database._get_conn().execute( + "UPDATE tasks SET status = 'cancelled' WHERE id = ?", + (str(task.id),), + ) + database._get_conn().commit() + + assert store.renew_remote_lease( + task.id, + "exe-a", + claim["lease_token"], + ) is None + assert _lease_state(task.id) == "active" + database.reset_db_path() + + +def test_complete_remote_task_requires_finalizing_lease(tmp_path, monkeypatch): + runtime = RuntimeSettings(data_root=str(tmp_path)) + monkeypatch.setattr(settings_module, "_runtime_settings", runtime) + database.reset_db_path(tmp_path) + store = database.get_task_store() + task = _task() + store.save(task) + store.register_worker(worker_id="exe-a", name="A") + claim = store.claim_remote_task("exe-a") + assert claim is not None + + completed, _already_completed = store.complete_remote_task( + task.id, + "exe-a", + claim["lease_token"], + {}, + ) + assert completed is None + assert store.begin_remote_upload(task.id, "exe-a", claim["lease_token"]) + completed, _already_completed = store.complete_remote_task( + task.id, + "exe-a", + claim["lease_token"], + {}, + ) + assert completed is None + assert store.begin_remote_finalization(task.id, "exe-a", claim["lease_token"]) + completed, already_completed = store.complete_remote_task( + task.id, + "exe-a", + claim["lease_token"], + {}, + ) + assert completed is not None + assert completed.status == TaskStatus.COMPLETED + assert already_completed is False + database.reset_db_path() + + +def test_archive_upload_finishes_task_and_is_idempotent(sync_client): + client, _queue, _tmp_path = sync_client + created = client.post( + "/api/tasks", + json={ + "task_type": "pipeline", + "source": "https://example.com/video", + "origin_client": "apk", + "requested_executor": "exe", + }, + ).json() + claim = _register_and_claim(client) + assert claim["task"]["id"] == created["id"] + + archive_bytes = _zip_bytes( + { + "metadata.json": json.dumps( + { + "title": "Remote result", + "status": "processing", + "extra": { + "downloaded_image_paths": [ + "D:/MPP/local-result/images/00.jpg", + ], + }, + } + ), + "summary.md": "# Summary\nDone\n", + "images/00.jpg": b"jpeg-data", + "analysis.json": json.dumps( + { + "image_descriptions": [ + { + "index": 0, + "image_path": "D:/MPP/local-result/images/00.jpg", + }, + ], + } + ), + } + ) + portable_result = { + "image_descriptions": [ + { + "index": 0, + "image_path": "images/00.jpg", + "text": "图片说明", + }, + ], + "analysis": { + "image_descriptions": [ + { + "index": 0, + "image_path": "images/00.jpg", + }, + ], + }, + "warnings": [{"code": "test-warning"}], + "output_dir": "D:/MPP/local-result", + "remote_sync": {"api_token": "must-not-merge"}, + } + endpoint = f"/api/workers/exe-test/tasks/{created['id']}/archive" + completed = client.post( + endpoint, + data={ + "lease_token": claim["lease_token"], + "result_json": json.dumps(portable_result, ensure_ascii=False), + }, + files={"archive": ("result.zip", archive_bytes, "application/zip")}, + ) + assert completed.status_code == 200, completed.text + payload = completed.json() + assert payload["already_completed"] is False + assert payload["task"]["status"] == "completed" + assert payload["task"]["assigned_executor"] == "exe-test" + assert payload["task"]["sync_revision"] >= 2 + + output_dir = Path(payload["task"]["result"]["output_dir"]) + assert (output_dir / "summary.md").read_text(encoding="utf-8") == "# Summary\nDone\n" + metadata = json.loads((output_dir / "metadata.json").read_text(encoding="utf-8")) + assert metadata["status"] == "completed" + assert metadata["task_id"] == created["id"] + expected_image = str((output_dir / "images" / "00.jpg").resolve()) + assert metadata["extra"]["downloaded_image_paths"] == [expected_image] + analysis = json.loads((output_dir / "analysis.json").read_text(encoding="utf-8")) + assert analysis["image_descriptions"][0]["image_path"] == expected_image + result = payload["task"]["result"] + assert result["image_descriptions"][0]["image_path"] == expected_image + assert result["analysis"]["image_descriptions"][0]["image_path"] == expected_image + assert result["warnings"] == [{"code": "test-warning"}] + assert "api_token" not in json.dumps(result) + + repeated = client.post( + endpoint, + data={"lease_token": claim["lease_token"]}, + files={"archive": ("result.zip", archive_bytes, "application/zip")}, + ) + assert repeated.status_code == 200 + assert repeated.json()["already_completed"] is True + + +def test_cancel_during_archive_upload_prevents_publish(sync_client, monkeypatch): + client, _queue, _tmp_path = sync_client + created = client.post( + "/api/tasks", + json={ + "task_type": "pipeline", + "source": "https://example.com/video", + "requested_executor": "exe", + }, + ).json() + claim = _register_and_claim(client) + task_id = UUID(created["id"]) + output_dir = Path(created["result"]["output_dir"]) + archive_bytes = _zip_bytes( + { + "metadata.json": json.dumps({"title": "Cancelled upload"}), + "summary.md": "# This must not be published", + } + ) + original_stream_upload = sync.stream_upload_to_path + control_queue = TaskQueue() + + async def cancel_after_upload_stream(upload, destination, **kwargs): + streamed = await original_stream_upload(upload, destination, **kwargs) + assert await control_queue.cancel(task_id) + return streamed + + monkeypatch.setattr(sync, "stream_upload_to_path", cancel_after_upload_stream) + response = client.post( + f"/api/workers/exe-test/tasks/{task_id}/archive", + data={"lease_token": claim["lease_token"]}, + files={"archive": ("result.zip", archive_bytes, "application/zip")}, + ) + + assert response.status_code == 409 + assert client.get(f"/api/tasks/{task_id}").json()["status"] == "cancelled" + assert _lease_state(task_id) == "cancelled" + assert not (output_dir / "summary.md").exists() + + +def test_archive_upload_rejects_path_traversal(sync_client): + client, _queue, tmp_path = sync_client + created = client.post( + "/api/tasks", + json={ + "task_type": "pipeline", + "source": "https://example.com/video", + "requested_executor": "exe", + }, + ).json() + claim = _register_and_claim(client) + archive_bytes = _zip_bytes( + { + "metadata.json": "{}", + "../escaped.txt": "blocked", + } + ) + response = client.post( + f"/api/workers/exe-test/tasks/{created['id']}/archive", + data={"lease_token": claim["lease_token"]}, + files={"archive": ("unsafe.zip", archive_bytes, "application/zip")}, + ) + assert response.status_code == 400 + assert not (tmp_path.parent / "escaped.txt").exists() + task = client.get(f"/api/tasks/{created['id']}").json() + assert task["status"] == "processing" + assert _lease_state(created["id"]) == "active" + + +def test_safe_extract_rejects_symlink(tmp_path): + zip_path = tmp_path / "symlink.zip" + with zipfile.ZipFile(zip_path, "w") as archive: + archive.writestr("metadata.json", "{}") + link = zipfile.ZipInfo("link") + link.create_system = 3 + link.external_attr = (0o120777 << 16) + archive.writestr(link, "../outside") + + with pytest.raises(ValueError, match="symbolic links"): + safe_extract_zip(zip_path, tmp_path / "extract") + + +def test_safe_extract_limits_structured_json_size(tmp_path, monkeypatch): + monkeypatch.setattr(archive_sync, "MAX_STRUCTURED_JSON_BYTES", 8) + zip_path = tmp_path / "large-json.zip" + with zipfile.ZipFile(zip_path, "w") as archive: + archive.writestr("metadata.json", '{"title":"too large"}') + + with pytest.raises(ValueError, match="structured JSON member is too large"): + safe_extract_zip(zip_path, tmp_path / "extract") + + +def test_sync_storage_sweep_recovers_backup_and_removes_stale_staging(tmp_path): + staging = tmp_path / "_remote_sync" / "abandoned-upload" + staging.mkdir(parents=True) + (staging / "archive.zip").write_bytes(b"partial") + os.utime(staging, (1, 1)) + + backup = tmp_path / f".recovered.sync-backup-{'a' * 32}" + backup.mkdir() + (backup / "metadata.json").write_text("{}", encoding="utf-8") + + stale_backup = tmp_path / f".current.sync-backup-{'b' * 32}" + stale_backup.mkdir() + os.utime(stale_backup, (1, 1)) + current = tmp_path / "current" + current.mkdir() + + result = archive_sync.sweep_stale_sync_storage( + tmp_path, + max_age_seconds=0, + ) + + assert result == {"removed": 2, "restored": 1} + assert not staging.exists() + assert not stale_backup.exists() + assert (tmp_path / "recovered" / "metadata.json").is_file() + + +def test_archive_download_excludes_media_by_default(sync_client): + client, _queue, tmp_path = sync_client + archive_dir = tmp_path / "completed" + (archive_dir / "images").mkdir(parents=True) + (archive_dir / "metadata.json").write_text( + json.dumps({"title": "Portable"}), + encoding="utf-8", + ) + (archive_dir / "summary.md").write_text("# Portable", encoding="utf-8") + (archive_dir / "images" / "00.jpg").write_bytes(b"image") + (archive_dir / "video.mp4").write_bytes(b"video") + + task = _task( + executor="server", + status=TaskStatus.COMPLETED, + result={ + "output_dir": str(archive_dir), + "metadata": {"title": "Portable"}, + }, + ) + database.get_task_store().save(task) + + without_media = client.get(f"/api/sync/tasks/{task.id}/archive") + assert without_media.status_code == 200 + with zipfile.ZipFile(io.BytesIO(without_media.content)) as archive: + names = set(archive.namelist()) + assert "metadata.json" in names + assert "summary.md" in names + assert "images/00.jpg" in names + assert "video.mp4" not in names + manifest = json.loads(archive.read("_sync_manifest.json")) + assert manifest["include_media"] is False + + with_media = client.get( + f"/api/sync/tasks/{task.id}/archive", + params={"include_media": "true"}, + ) + assert with_media.status_code == 200 + with zipfile.ZipFile(io.BytesIO(with_media.content)) as archive: + assert "video.mp4" in archive.namelist() diff --git a/tests/test_remote_sync_client.py b/tests/test_remote_sync_client.py new file mode 100644 index 0000000..403988c --- /dev/null +++ b/tests/test_remote_sync_client.py @@ -0,0 +1,900 @@ +from __future__ import annotations + +import asyncio +import io +import json +import sys +import zipfile +from pathlib import Path +from uuid import UUID, uuid4 + +import httpx +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient + +ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(ROOT / "backend")) + +from app.api.routes import tasks # noqa: E402 +from app.core import database # noqa: E402 +from app.core import settings as settings_module # noqa: E402 +from app.core.queue import REMOTE_MIRROR_OPTION, TaskQueue # noqa: E402 +from app.core.settings import RuntimeSettings # noqa: E402 +from app.models import Task, TaskCreate, TaskStatus, TaskType # noqa: E402 +from app.models.task import REMOTE_ARCHIVE_REVISION_OPTION # noqa: E402 +from app.services import remote_sync as remote_sync_module # noqa: E402 +from app.services.remote_sync import RemoteSyncService, _RemoteConfig # noqa: E402 + + +class _FakeRemoteQueue: + def __init__( + self, + store, + output_dir: Path | None = None, + *, + revision_updates: int = 0, + ) -> None: + self.store = store + self.output_dir = output_dir + self.revision_updates = revision_updates + self.submitted: list[tuple[UUID, str]] = [] + self.cancelled: list[UUID] = [] + self.released: list[tuple[UUID, str]] = [] + + async def submit_remote_claim(self, task_id: UUID, worker_id: str) -> None: + self.submitted.append((task_id, worker_id)) + if self.output_dir is not None: + task = self.store.get(task_id) + assert task is not None + task.status = TaskStatus.COMPLETED + task.completed_at = task.updated_at + task.result = {"output_dir": str(self.output_dir)} + self.store.save(task) + for _ in range(self.revision_updates): + self.store.update_status( + task_id, + TaskStatus.COMPLETED, + progress=1.0, + ) + + async def cancel(self, task_id: UUID) -> bool: + self.cancelled.append(task_id) + return True + + def release_remote_claim(self, task_id: UUID, worker_id: str) -> bool: + self.released.append((task_id, worker_id)) + return True + + +def _runtime(tmp_path: Path, **updates) -> RuntimeSettings: + values = { + "data_root": str(tmp_path), + "remote_sync_enabled": True, + "remote_server_url": "https://coordinator.example", + "remote_worker_id": "desktop-test", + "remote_worker_name": "Desktop Test", + "remote_sync_interval_sec": 5, + } + values.update(updates) + return RuntimeSettings(**values) + + +def _async_client(transport: httpx.MockTransport) -> httpx.AsyncClient: + return httpx.AsyncClient( + base_url="https://coordinator.example/api/", + transport=transport, + headers={"X-Requested-With": "test"}, + ) + + +def _zip_bytes(files: dict[str, str]) -> bytes: + output = io.BytesIO() + with zipfile.ZipFile(output, "w", compression=zipfile.ZIP_DEFLATED) as archive: + for filename, content in files.items(): + archive.writestr(filename, content) + return output.getvalue() + + +def test_remote_worker_name_defaults_to_hostname(tmp_path, monkeypatch): + monkeypatch.setattr(remote_sync_module.socket, "gethostname", lambda: "desktop-host") + + config = _RemoteConfig.from_settings( + _runtime(tmp_path, remote_worker_name="", remote_worker_id="") + ) + + assert config.worker_name == "desktop-host" + + +@pytest.mark.asyncio +async def test_remote_sync_service_start_and_stop_manage_runner(tmp_path, monkeypatch): + runtime = _runtime(tmp_path) + service = RemoteSyncService(settings_getter=lambda: runtime) + started = asyncio.Event() + cancelled = asyncio.Event() + + async def fake_run(): + started.set() + try: + await asyncio.Future() + finally: + cancelled.set() + + monkeypatch.setattr(service, "_run", fake_run) + + await service.start() + await started.wait() + await service.stop() + + assert cancelled.is_set() + + +@pytest.mark.asyncio +async def test_forward_task_uses_default_executor_and_saves_safe_mirror( + tmp_path, + monkeypatch, +): + runtime = _runtime(tmp_path, default_task_executor="exe") + database.reset_db_path(tmp_path) + store = database.get_task_store() + requests: list[dict] = [] + task_id = uuid4() + + async def handler(request: httpx.Request) -> httpx.Response: + payload = json.loads((await request.aread()).decode("utf-8")) + if request.url.path.endswith("/workers/register"): + return httpx.Response( + 200, + json={"worker": {"id": "desktop-test"}}, + ) + requests.append(payload) + return httpx.Response( + 200, + json={ + "id": str(task_id), + "task_type": "pipeline", + "status": "queued", + "source": payload["source"], + "options": payload["options"], + "origin_client": payload["origin_client"], + "requested_executor": payload["requested_executor"], + "result": {"output_dir": "/srv/mpp/task"}, + }, + ) + + transport = httpx.MockTransport(handler) + service = RemoteSyncService( + settings_getter=lambda: runtime, + store_getter=lambda: store, + ) + monkeypatch.setattr(service, "_make_client", lambda _config: _async_client(transport)) + + task = await service.forward_task( + TaskCreate(task_type=TaskType.PIPELINE, source="https://example.com/video") + ) + + assert requests[0]["origin_client"] == "exe" + assert requests[0]["requested_executor"] == "exe" + assert task.id == task_id + assert task.options[REMOTE_MIRROR_OPTION] is True + assert "output_dir" not in (task.result or {}) + assert task.result["remote_sync"]["remote_output_dir"] == "/srv/mpp/task" + assert store.get(task_id) == task + database.reset_db_path() + + +@pytest.mark.asyncio +async def test_forward_local_file_targets_only_registered_exe(tmp_path, monkeypatch): + runtime = _runtime(tmp_path, default_task_executor="exe") + media_path = tmp_path / "local-video.mp4" + media_path.write_bytes(b"video") + database.reset_db_path(tmp_path) + store = database.get_task_store() + requests: list[dict] = [] + + async def handler(request: httpx.Request) -> httpx.Response: + payload = json.loads((await request.aread()).decode("utf-8")) + if request.url.path.endswith("/workers/register"): + return httpx.Response( + 200, + json={"worker": {"id": "desktop-test"}}, + ) + requests.append(payload) + return httpx.Response( + 200, + json={ + "task_type": "pipeline", + "status": "queued", + "source": payload["source"], + "options": payload["options"], + "origin_client": "exe", + "requested_executor": "exe", + "assigned_executor": "desktop-test", + }, + ) + + service = RemoteSyncService( + settings_getter=lambda: runtime, + store_getter=lambda: store, + ) + monkeypatch.setattr( + service, + "_make_client", + lambda _config: _async_client(httpx.MockTransport(handler)), + ) + + task = await service.forward_task( + TaskCreate(task_type=TaskType.PIPELINE, source=str(media_path)) + ) + + assert requests[0]["requested_executor"] == "exe" + assert requests[0]["options"]["_mpp_preferred_worker_id"] == "desktop-test" + assert task.assigned_executor == "desktop-test" + with pytest.raises( + remote_sync_module.RemoteSyncConfigurationError, + match="只能指派给 EXE", + ): + await service.forward_task( + TaskCreate( + task_type=TaskType.PIPELINE, + source=str(media_path), + requested_executor="server", + ) + ) + database.reset_db_path() + + +@pytest.mark.parametrize("use_local_source", [False, True], ids=["url", "local-file"]) +@pytest.mark.asyncio +async def test_claimed_task_uploads_archive_and_preserves_uuid( + tmp_path, + use_local_source, +): + runtime = _runtime(tmp_path) + database.reset_db_path(tmp_path) + store = database.get_task_store() + archive_dir = tmp_path / "local-result" + archive_dir.mkdir() + (archive_dir / "metadata.json").write_text( + json.dumps({"title": "Local result"}), + encoding="utf-8", + ) + (archive_dir / "summary.md").write_text("# Done", encoding="utf-8") + queue = _FakeRemoteQueue(store, archive_dir, revision_updates=12) + task_id = uuid4() + upload_requests = 0 + local_media = tmp_path / "claimed-local.mp4" + local_media.write_bytes(b"video") + source = str(local_media) if use_local_source else "https://example.com/video" + + async def handler(request: httpx.Request) -> httpx.Response: + nonlocal upload_requests + if request.url.path.endswith("/archive"): + upload_requests += 1 + body = await request.aread() + assert b"metadata.json" in body + return httpx.Response( + 200, + json={ + "task": { + "id": str(task_id), + "sync_revision": 3, + }, + "already_completed": False, + }, + ) + return httpx.Response(500) + + service = RemoteSyncService( + settings_getter=lambda: runtime, + store_getter=lambda: store, + queue_getter=lambda: queue, + ) + claim = { + "task": { + "id": str(task_id), + "task_type": "pipeline", + "status": "processing", + "source": source, + "options": {"_mpp_preferred_worker_id": "desktop-test"}, + "origin_client": "apk", + "requested_executor": "exe", + "assigned_executor": "desktop-test", + }, + "lease_token": "lease-token-1234567890", + "lease_expires_at": "2099-01-01T00:00:00+00:00", + } + + async with _async_client(httpx.MockTransport(handler)) as client: + await service._process_claim( + client, + _RemoteConfig.from_settings(runtime), + "desktop-test", + claim, + ) + + saved = store.get(task_id) + assert upload_requests == 1 + assert queue.submitted == [(task_id, "desktop-test")] + assert queue.released == [(task_id, "desktop-test")] + assert saved is not None + assert saved.sync_revision == 3 + assert saved.source == source + assert saved.result["output_dir"] == str(archive_dir) + assert "_mpp_preferred_worker_id" not in saved.options + assert saved.options[REMOTE_ARCHIVE_REVISION_OPTION] == 3 + database.reset_db_path() + + +@pytest.mark.asyncio +async def test_claimed_missing_local_file_is_reported_without_queueing(tmp_path): + runtime = _runtime(tmp_path) + database.reset_db_path(tmp_path) + store = database.get_task_store() + queue = _FakeRemoteQueue(store) + task_id = uuid4() + failures: list[dict] = [] + + async def handler(request: httpx.Request) -> httpx.Response: + if request.url.path.endswith("/fail"): + failures.append(json.loads((await request.aread()).decode("utf-8"))) + return httpx.Response(200, json={"task": {"id": str(task_id)}}) + return httpx.Response(500) + + service = RemoteSyncService( + settings_getter=lambda: runtime, + store_getter=lambda: store, + queue_getter=lambda: queue, + ) + claim = { + "task": { + "id": str(task_id), + "task_type": "pipeline", + "status": "processing", + "source": str(tmp_path / "missing.mp4"), + "origin_client": "exe", + "requested_executor": "exe", + "assigned_executor": "desktop-test", + }, + "lease_token": "lease-token-1234567890", + "lease_expires_at": "2099-01-01T00:00:00+00:00", + } + + async with _async_client(httpx.MockTransport(handler)) as client: + await service._process_claim( + client, + _RemoteConfig.from_settings(runtime), + "desktop-test", + claim, + ) + + assert len(failures) == 1 + assert "可读取" in failures[0]["error"] + assert queue.submitted == [] + assert store.get(task_id) is None + database.reset_db_path() + + +@pytest.mark.asyncio +async def test_claimed_server_staged_input_is_downloaded_atomically_before_queue( + tmp_path, +): + runtime = _runtime(tmp_path) + database.reset_db_path(tmp_path) + store = database.get_task_store() + archive_dir = tmp_path / "local-result" + archive_dir.mkdir() + (archive_dir / "metadata.json").write_text( + json.dumps({"title": "Local result"}), + encoding="utf-8", + ) + source_seen: list[Path] = [] + source_bytes: list[bytes] = [] + + class InspectingQueue(_FakeRemoteQueue): + async def submit_remote_claim(self, task_id: UUID, worker_id: str) -> None: + task = self.store.get(task_id) + assert task is not None + source = Path(task.source) + source_seen.append(source) + source_bytes.append(source.read_bytes()) + assert not list(source.parent.glob("*.part")) + await super().submit_remote_claim(task_id, worker_id) + + queue = InspectingQueue(store, archive_dir) + task_id = uuid4() + media = b"media uploaded through coordinator" + input_requests = 0 + + async def handler(request: httpx.Request) -> httpx.Response: + nonlocal input_requests + if request.method == "GET" and request.url.path.endswith("/input"): + input_requests += 1 + assert request.headers["X-MPP-Lease-Token"] == "lease-token-1234567890" + return httpx.Response(200, content=media) + if request.url.path.endswith("/heartbeat"): + return httpx.Response(200, json={"worker": {"id": "desktop-test"}}) + if request.method == "GET" and request.url.path.endswith("/tasks"): + return httpx.Response(200, json=[]) + if request.url.path.endswith("/archive"): + return httpx.Response( + 200, + json={ + "task": {"id": str(task_id), "sync_revision": 4}, + "already_completed": False, + }, + ) + return httpx.Response(500) + + service = RemoteSyncService( + settings_getter=lambda: runtime, + store_getter=lambda: store, + queue_getter=lambda: queue, + ) + claim = { + "task": { + "id": str(task_id), + "task_type": "pipeline", + "status": "processing", + "source": "/srv/mpp/data/_staging/upload/phone.mp4", + "origin_client": "apk", + "requested_executor": "exe", + "assigned_executor": "desktop-test", + }, + "input": {"filename": "../phone.mp4", "size": len(media)}, + "lease_token": "lease-token-1234567890", + "lease_expires_at": "2099-01-01T00:00:00+00:00", + } + + async with _async_client(httpx.MockTransport(handler)) as client: + await service._process_claim( + client, + _RemoteConfig.from_settings(runtime), + "desktop-test", + claim, + ) + + assert input_requests == 1 + assert source_bytes == [media] + assert source_seen[0].name == "phone.mp4" + assert source_seen[0].is_relative_to(tmp_path / "_staging") + assert not source_seen[0].exists() + assert queue.submitted == [(task_id, "desktop-test")] + assert queue.released == [(task_id, "desktop-test")] + database.reset_db_path() + + +@pytest.mark.asyncio +async def test_long_claim_keeps_heartbeat_and_remote_mirror_running( + tmp_path, + monkeypatch, +): + runtime = _runtime(tmp_path) + database.reset_db_path(tmp_path) + store = database.get_task_store() + archive_dir = tmp_path / "long-result" + archive_dir.mkdir() + (archive_dir / "metadata.json").write_text("{}", encoding="utf-8") + queue = _FakeRemoteQueue(store) + task_id = uuid4() + heartbeat_seen = asyncio.Event() + mirror_seen = asyncio.Event() + mirror_calls = 0 + + async def handler(request: httpx.Request) -> httpx.Response: + if request.url.path.endswith("/heartbeat"): + heartbeat_seen.set() + return httpx.Response(200, json={"worker": {"id": "desktop-test"}}) + if request.url.path.endswith("/archive"): + return httpx.Response( + 200, + json={ + "task": {"id": str(task_id), "sync_revision": 2}, + "already_completed": False, + }, + ) + return httpx.Response(500) + + service = RemoteSyncService( + settings_getter=lambda: runtime, + store_getter=lambda: store, + queue_getter=lambda: queue, + ) + + async def record_mirror(_client, _config): + nonlocal mirror_calls + mirror_calls += 1 + if mirror_calls >= 2: + mirror_seen.set() + + async def finish_after_guards(task_id_arg, lease_lost): + assert task_id_arg == task_id + await asyncio.wait_for( + asyncio.gather(heartbeat_seen.wait(), mirror_seen.wait()), + timeout=2, + ) + assert not lease_lost.is_set() + task = store.get(task_id) + assert task is not None + task.status = TaskStatus.COMPLETED + task.result = {"output_dir": str(archive_dir)} + store.save(task) + return task + + monkeypatch.setattr(service, "_mirror_remote_tasks", record_mirror) + monkeypatch.setattr(service, "_wait_for_local_terminal", finish_after_guards) + config = _RemoteConfig( + api_base="https://coordinator.example/api/", + api_token="", + worker_id="desktop-test", + worker_name="Desktop Test", + interval_sec=0.01, + upload_results=True, + download_results=True, + include_media=False, + ) + claim = { + "task": { + "id": str(task_id), + "task_type": "pipeline", + "status": "processing", + "source": "https://example.com/long-video", + "origin_client": "apk", + "requested_executor": "exe", + "assigned_executor": "desktop-test", + }, + "lease_token": "lease-token-1234567890", + "lease_expires_at": "2099-01-01T00:00:00+00:00", + } + + async with _async_client(httpx.MockTransport(handler)) as client: + await service._process_claim(client, config, "desktop-test", claim) + + assert heartbeat_seen.is_set() + assert mirror_calls >= 2 + assert queue.submitted == [(task_id, "desktop-test")] + assert queue.released == [(task_id, "desktop-test")] + database.reset_db_path() + + +@pytest.mark.asyncio +async def test_completed_remote_archive_download_is_not_repeated(tmp_path): + runtime = _runtime(tmp_path, remote_sync_download_results=True) + database.reset_db_path(tmp_path) + store = database.get_task_store() + task_id = uuid4() + archive_downloads = 0 + archive_bytes = _zip_bytes( + { + "metadata.json": json.dumps( + { + "title": "Remote archive", + "extra": { + "downloaded_image_paths": [ + "/srv/mpp/result/images/00.jpg", + ], + }, + } + ), + "summary.md": "# Remote", + "images/00.jpg": "jpeg-data", + "analysis.json": json.dumps( + { + "image_descriptions": [ + { + "index": 0, + "image_path": "/srv/mpp/result/images/00.jpg", + }, + ], + } + ), + } + ) + remote_task = Task( + id=task_id, + task_type=TaskType.PIPELINE, + status=TaskStatus.COMPLETED, + source="https://example.com/video", + requested_executor="server", + assigned_executor="server", + sync_revision=10, + result={ + "output_dir": "/srv/mpp/result", + "metadata": { + "title": "Remote archive", + "extra": { + "downloaded_image_paths": [ + "/srv/mpp/result/images/00.jpg", + ], + }, + }, + "image_descriptions": [ + { + "index": 0, + "image_path": "/srv/mpp/result/images/00.jpg", + "text": "图片说明", + }, + ], + }, + ) + stale_mirror = remote_task.model_copy(deep=True) + stale_mirror.status = TaskStatus.PROCESSING + stale_mirror.sync_revision = 2 + stale_mirror.options = {REMOTE_MIRROR_OPTION: True} + stale_mirror.result = None + store.save_remote_mirror(stale_mirror) + + async def handler(request: httpx.Request) -> httpx.Response: + nonlocal archive_downloads + if request.url.path.endswith("/archive"): + archive_downloads += 1 + return httpx.Response( + 200, + content=archive_bytes, + headers={"content-type": "application/zip"}, + ) + return httpx.Response(500) + + service = RemoteSyncService( + settings_getter=lambda: runtime, + store_getter=lambda: store, + ) + config = _RemoteConfig.from_settings(runtime) + async with _async_client(httpx.MockTransport(handler)) as client: + await service._mirror_remote_task(client, config, remote_task) + await service._mirror_remote_task(client, config, remote_task) + + saved = store.get(task_id) + assert archive_downloads == 1 + assert saved is not None + assert saved.sync_revision == 10 + local_output = Path(saved.result["output_dir"]) + assert local_output == tmp_path / f"remote-{task_id}" + assert (local_output / "summary.md").read_text(encoding="utf-8") == "# Remote" + expected_image = str((local_output / "images" / "00.jpg").resolve()) + metadata = json.loads((local_output / "metadata.json").read_text(encoding="utf-8")) + analysis = json.loads((local_output / "analysis.json").read_text(encoding="utf-8")) + assert metadata["extra"]["downloaded_image_paths"] == [expected_image] + assert analysis["image_descriptions"][0]["image_path"] == expected_image + assert saved.result["image_descriptions"][0]["image_path"] == expected_image + assert saved.options[REMOTE_ARCHIVE_REVISION_OPTION] == 10 + database.reset_db_path() + + +@pytest.mark.asyncio +async def test_reenabling_archive_download_fetches_revision_skipped_while_disabled( + tmp_path, +): + database.reset_db_path(tmp_path) + store = database.get_task_store() + task_id = uuid4() + local_output = tmp_path / f"remote-{task_id}" + local_output.mkdir() + (local_output / "metadata.json").write_text( + json.dumps({"title": "Old archive"}), + encoding="utf-8", + ) + (local_output / "summary.md").write_text("# Old", encoding="utf-8") + + remote_task = Task( + id=task_id, + task_type=TaskType.PIPELINE, + status=TaskStatus.COMPLETED, + source="https://example.com/video", + sync_revision=5, + result={ + "output_dir": "/srv/mpp/result", + "metadata": {"title": "New archive"}, + }, + ) + local_task = remote_task.model_copy(deep=True) + local_task.sync_revision = 4 + local_task.options = { + REMOTE_MIRROR_OPTION: True, + REMOTE_ARCHIVE_REVISION_OPTION: 2, + } + local_task.result = { + "output_dir": str(local_output), + "metadata": {"title": "Old archive"}, + } + store.save_remote_mirror(local_task) + + archive_downloads = 0 + archive_bytes = _zip_bytes( + { + "metadata.json": json.dumps({"title": "New archive"}), + "summary.md": "# New", + } + ) + + async def handler(request: httpx.Request) -> httpx.Response: + nonlocal archive_downloads + if request.url.path.endswith("/archive"): + archive_downloads += 1 + return httpx.Response( + 200, + content=archive_bytes, + headers={"content-type": "application/zip"}, + ) + return httpx.Response(500) + + service = RemoteSyncService( + settings_getter=lambda: _runtime(tmp_path), + store_getter=lambda: store, + ) + download_disabled = _RemoteConfig.from_settings( + _runtime(tmp_path, remote_sync_download_results=False) + ) + download_enabled = _RemoteConfig.from_settings( + _runtime(tmp_path, remote_sync_download_results=True) + ) + async with _async_client(httpx.MockTransport(handler)) as client: + await service._mirror_remote_task(client, download_disabled, remote_task) + skipped = store.get(task_id) + assert skipped is not None + assert skipped.sync_revision == 5 + assert skipped.options[REMOTE_ARCHIVE_REVISION_OPTION] == 2 + assert (local_output / "summary.md").read_text(encoding="utf-8") == "# Old" + assert archive_downloads == 0 + + await service._mirror_remote_task(client, download_enabled, remote_task) + + saved = store.get(task_id) + assert saved is not None + assert archive_downloads == 1 + assert saved.sync_revision == 5 + assert saved.options[REMOTE_ARCHIVE_REVISION_OPTION] == 5 + assert (local_output / "summary.md").read_text(encoding="utf-8") == "# New" + database.reset_db_path() + + +@pytest.mark.asyncio +async def test_remote_task_mirroring_fetches_every_page(tmp_path, monkeypatch): + runtime = _runtime(tmp_path) + service = RemoteSyncService(settings_getter=lambda: runtime) + tasks_payload = [ + Task( + task_type=TaskType.PIPELINE, + status=TaskStatus.QUEUED, + source=f"https://example.com/{index}", + ).model_dump(mode="json") + for index in range(450) + ] + poison_id = UUID(tasks_payload[10]["id"]) + requested_offsets: list[int] = [] + mirrored_ids: list[UUID] = [] + + async def handler(request: httpx.Request) -> httpx.Response: + offset = int(request.url.params.get("offset", "0")) + limit = int(request.url.params.get("limit", "50")) + requested_offsets.append(offset) + return httpx.Response( + 200, + json=tasks_payload[offset : offset + limit], + ) + + async def record_mirror(_client, _config, remote_task): + if remote_task.id == poison_id: + raise OSError("damaged remote archive") + mirrored_ids.append(remote_task.id) + + monkeypatch.setattr(service, "_mirror_remote_task", record_mirror) + async with _async_client(httpx.MockTransport(handler)) as client: + await service._mirror_remote_tasks( + client, + _RemoteConfig.from_settings(runtime), + ) + + assert requested_offsets == [0, 200, 400] + assert len(mirrored_ids) == 449 + assert len(set(mirrored_ids)) == 449 + assert poison_id not in mirrored_ids + + +@pytest.mark.asyncio +async def test_queue_requires_explicit_remote_claim_grant(tmp_path, monkeypatch): + runtime = _runtime(tmp_path) + monkeypatch.setattr(settings_module, "_runtime_settings", runtime) + database.reset_db_path(tmp_path) + store = database.get_task_store() + task = Task( + task_type=TaskType.PIPELINE, + status=TaskStatus.PROCESSING, + source="https://example.com/video", + options={REMOTE_MIRROR_OPTION: True}, + requested_executor="exe", + assigned_executor="desktop-test", + ) + store.save(task) + queue = TaskQueue() + + with pytest.raises(ValueError, match="not assigned"): + await queue.submit_remote_claim(task.id, "another-worker") + await queue.submit_remote_claim(task.id, "desktop-test") + + assert queue.get_queue_snapshot() == [task.id] + assert queue.release_remote_claim(task.id, "desktop-test") is True + assert queue._is_locally_runnable(store.get(task.id)) is False + database.reset_db_path() + + +def test_task_and_batch_forwarding_preserve_default_executor(tmp_path, monkeypatch): + runtime = _runtime(tmp_path, default_task_executor="exe") + monkeypatch.setattr(settings_module, "_runtime_settings", runtime) + database.reset_db_path(tmp_path) + forwarded: list[TaskCreate] = [] + local_path = tmp_path / "local.mp4" + local_path.write_bytes(b"video") + + class FakeService: + async def forward_task(self, request: TaskCreate) -> Task: + if ( + not request.source.startswith(("http://", "https://")) + and request.requested_executor == "server" + ): + raise remote_sync_module.RemoteSyncConfigurationError( + "本地文件只能指派给 EXE" + ) + forwarded.append(request) + return Task( + task_type=request.task_type, + status=TaskStatus.QUEUED, + source=request.source, + options={REMOTE_MIRROR_OPTION: True}, + requested_executor=request.requested_executor, + origin_client="exe", + ) + + monkeypatch.setattr( + remote_sync_module, + "get_remote_sync_service", + lambda: FakeService(), + ) + app = FastAPI() + app.include_router(tasks.router, prefix="/api") + + with TestClient(app) as client: + single = client.post( + "/api/tasks", + json={ + "task_type": "pipeline", + "source": "https://example.com/single", + }, + ) + batch = client.post( + "/api/tasks/batch", + json={ + "task_type": "pipeline", + "sources": [ + "https://example.com/one", + str(local_path), + ], + }, + ) + local_file = client.post( + "/api/tasks", + json={ + "task_type": "pipeline", + "source": str(local_path), + }, + ) + server_local_file = client.post( + "/api/tasks", + json={ + "task_type": "pipeline", + "source": str(local_path), + "requested_executor": "server", + }, + ) + + assert single.status_code == 200 + assert batch.status_code == 200 + assert local_file.status_code == 200 + assert server_local_file.status_code == 400 + assert [request.requested_executor for request in forwarded] == [ + "exe", + "exe", + "exe", + "exe", + ] + database.reset_db_path() diff --git a/tests/test_thumbnail_helpers.py b/tests/test_thumbnail_helpers.py new file mode 100644 index 0000000..3c6d447 --- /dev/null +++ b/tests/test_thumbnail_helpers.py @@ -0,0 +1,221 @@ +from __future__ import annotations + +import io +import shutil +import sys +from pathlib import Path + +import pytest +from fastapi import FastAPI +from fastapi.responses import FileResponse +from fastapi.testclient import TestClient + +ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(ROOT / "backend")) + +from app.api.routes import pipeline as pipeline_route # noqa: E402 +from app.core.settings import RuntimeSettings # noqa: E402 +from app.services.archiving import thumbnails # noqa: E402 + + +def _pipeline_client(tmp_path: Path, monkeypatch) -> TestClient: + monkeypatch.setattr( + pipeline_route, + "get_runtime_settings", + lambda: RuntimeSettings(data_root=str(tmp_path)), + ) + app = FastAPI() + app.include_router(pipeline_route.router, prefix="/api") + return TestClient(app) + + +def _archive_image(tmp_path: Path, *, size: tuple[int, int] = (1600, 900)) -> Path: + Image = pytest.importorskip("PIL.Image") + archive_dir = tmp_path / "archive" + images_dir = archive_dir / "images" + images_dir.mkdir(parents=True) + (archive_dir / "metadata.json").write_text("{}", encoding="utf-8") + source = images_dir / "source.png" + Image.new("RGBA", size, color=(20, 120, 240, 128)).save(source, "PNG") + return source + + +def test_image_thumbnail_uses_ffmpeg_when_pillow_cannot_decode(tmp_path, monkeypatch): + calls: list[str] = [] + + def fake_pillow(data, thumb_path, *, size, quality): + calls.append("pillow") + return False + + def fake_ffmpeg(data, thumb_path, *, size): + calls.append("ffmpeg") + thumb_path.write_bytes(b"jpeg-thumbnail") + return True + + monkeypatch.setattr(thumbnails, "_create_pillow_thumbnail_from_bytes", fake_pillow) + monkeypatch.setattr(thumbnails, "_create_ffmpeg_thumbnail_from_bytes", fake_ffmpeg) + + result = thumbnails.create_image_thumbnail_from_bytes(b"source-image", tmp_path) + + assert calls == ["pillow", "ffmpeg"] + assert result == tmp_path / "thumbnail.jpg" + assert result.read_bytes() == b"jpeg-thumbnail" + + +def test_ffmpeg_image_thumbnail_fallback_produces_jpeg(tmp_path, monkeypatch): + if not shutil.which("ffmpeg"): + pytest.skip("ffmpeg is not installed") + Image = pytest.importorskip("PIL.Image") + source = io.BytesIO() + Image.new("RGB", (1200, 800), color=(10, 80, 220)).save(source, "PNG") + monkeypatch.setattr( + thumbnails, + "_create_pillow_thumbnail_from_bytes", + lambda *_args, **_kwargs: False, + ) + + result = thumbnails.create_image_thumbnail_from_bytes(source.getvalue(), tmp_path) + + assert result == tmp_path / "thumbnail.jpg" + with Image.open(result) as image: + assert image.format == "JPEG" + assert image.width <= 480 + assert image.height <= 480 + + +@pytest.mark.asyncio +async def test_archive_thumbnail_offloads_image_conversion(tmp_path, monkeypatch): + archive_dir = tmp_path / "archive" + archive_dir.mkdir() + (archive_dir / "cover.jpg").write_bytes(b"source-image") + settings = RuntimeSettings(data_root=str(tmp_path)) + offloaded: list[object] = [] + + def fake_create_image_thumbnail(source, destination): + thumb_path = destination / "thumbnail.jpg" + thumb_path.write_bytes(b"jpeg-thumbnail") + return thumb_path + + async def fake_to_thread(function, *args, **kwargs): + offloaded.append(function) + return function(*args, **kwargs) + + monkeypatch.setattr(pipeline_route, "get_runtime_settings", lambda: settings) + monkeypatch.setattr(pipeline_route, "create_image_thumbnail", fake_create_image_thumbnail) + monkeypatch.setattr(pipeline_route.asyncio, "to_thread", fake_to_thread) + + response = await pipeline_route.archive_thumbnail(str(archive_dir)) + + assert isinstance(response, FileResponse) + assert offloaded == [fake_create_image_thumbnail] + assert Path(response.path).read_bytes() == b"jpeg-thumbnail" + + +def test_archive_image_endpoint_returns_bounded_jpeg_with_cache_headers( + tmp_path, + monkeypatch, +): + Image = pytest.importorskip("PIL.Image") + source = _archive_image(tmp_path) + client = _pipeline_client(tmp_path, monkeypatch) + + response = client.get( + "/api/pipeline/archives/image", + params={"path": str(source), "max_edge": 512}, + ) + + assert response.status_code == 200 + assert response.headers["content-type"] == "image/jpeg" + assert response.headers["cache-control"] == "private, max-age=86400" + assert response.headers["x-content-type-options"] == "nosniff" + assert response.headers["vary"] == "Authorization" + assert response.headers["etag"].startswith('"') + with Image.open(io.BytesIO(response.content)) as resized: + assert resized.format == "JPEG" + assert resized.width == 512 + assert resized.height == 288 + assert resized.mode == "RGB" + + cached = client.get( + "/api/pipeline/archives/image", + params={"path": str(source), "max_edge": 512}, + headers={"If-None-Match": response.headers["etag"]}, + ) + assert cached.status_code == 304 + assert cached.content == b"" + + +def test_archive_image_endpoint_rejects_paths_outside_data_root( + tmp_path, + monkeypatch, +): + Image = pytest.importorskip("PIL.Image") + outside = tmp_path.parent / "outside-archive-image.png" + Image.new("RGB", (32, 32), color="red").save(outside, "PNG") + client = _pipeline_client(tmp_path, monkeypatch) + + response = client.get( + "/api/pipeline/archives/image", + params={"path": str(outside)}, + ) + + assert response.status_code == 403 + + +def test_archive_image_endpoint_requires_archive_marker(tmp_path, monkeypatch): + Image = pytest.importorskip("PIL.Image") + folder = tmp_path / "unregistered" + folder.mkdir() + source = folder / "image.png" + Image.new("RGB", (32, 32), color="red").save(source, "PNG") + client = _pipeline_client(tmp_path, monkeypatch) + + response = client.get( + "/api/pipeline/archives/image", + params={"path": str(source)}, + ) + + assert response.status_code == 403 + + +def test_archive_image_endpoint_rejects_invalid_content_and_parameters( + tmp_path, + monkeypatch, +): + archive_dir = tmp_path / "archive" + images_dir = archive_dir / "images" + images_dir.mkdir(parents=True) + (archive_dir / "metadata.json").write_text("{}", encoding="utf-8") + invalid = images_dir / "broken.png" + invalid.write_bytes(b"not-an-image") + client = _pipeline_client(tmp_path, monkeypatch) + + invalid_content = client.get( + "/api/pipeline/archives/image", + params={"path": str(invalid)}, + ) + invalid_edge = client.get( + "/api/pipeline/archives/image", + params={"path": str(invalid), "max_edge": 128}, + ) + invalid_quality = client.get( + "/api/pipeline/archives/image", + params={"path": str(invalid), "quality": 99}, + ) + + assert invalid_content.status_code == 415 + assert invalid_edge.status_code == 422 + assert invalid_quality.status_code == 422 + + +def test_archive_image_endpoint_enforces_source_size_limit(tmp_path, monkeypatch): + source = _archive_image(tmp_path, size=(64, 64)) + client = _pipeline_client(tmp_path, monkeypatch) + monkeypatch.setattr(thumbnails, "MAX_ARCHIVE_IMAGE_SOURCE_BYTES", 8) + + response = client.get( + "/api/pipeline/archives/image", + params={"path": str(source)}, + ) + + assert response.status_code == 413 diff --git a/tests/test_uvr_pipeline_guards.py b/tests/test_uvr_pipeline_guards.py index bd44922..782e7bc 100644 --- a/tests/test_uvr_pipeline_guards.py +++ b/tests/test_uvr_pipeline_guards.py @@ -1,7 +1,7 @@ import logging import sys from pathlib import Path -from types import SimpleNamespace +from types import ModuleType, SimpleNamespace import pytest from pydantic import ValidationError @@ -54,7 +54,12 @@ def __init__(self, **kwargs): def load_model(self, model_name: str) -> None: self.model_name = model_name - monkeypatch.setattr("audio_separator.separator.Separator", FakeSeparator) + audio_separator_module = ModuleType("audio_separator") + separator_module = ModuleType("audio_separator.separator") + separator_module.Separator = FakeSeparator + audio_separator_module.separator = separator_module + monkeypatch.setitem(sys.modules, "audio_separator", audio_separator_module) + monkeypatch.setitem(sys.modules, "audio_separator.separator", separator_module) monkeypatch.setattr( uvr_mod, "get_runtime_settings", diff --git a/tests/test_vlm_service.py b/tests/test_vlm_service.py new file mode 100644 index 0000000..ea46f93 --- /dev/null +++ b/tests/test_vlm_service.py @@ -0,0 +1,76 @@ +from __future__ import annotations + +import sys +from pathlib import Path +from types import SimpleNamespace + +import pytest + +ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(ROOT / "backend")) + +from app.core import settings as settings_module # noqa: E402 +from app.core.model_router import EndpointBinding # noqa: E402 +from app.services.analysis import vlm as vlm_module # noqa: E402 + + +class _FakeCompletions: + def __init__(self) -> None: + self.calls: list[dict] = [] + + def create(self, **kwargs): + self.calls.append(kwargs) + message = SimpleNamespace(content="KIND: content\n一张测试图片") + return SimpleNamespace(choices=[SimpleNamespace(message=message)]) + + +@pytest.mark.parametrize( + ("configured_max_tokens", "expected_max_tokens"), + [(1024, 1024), (64, 256)], +) +def test_describe_image_honors_configured_token_budget( + tmp_path, + monkeypatch, + configured_max_tokens, + expected_max_tokens, +): + image_path = tmp_path / "image.jpg" + image_path.write_bytes(b"image") + completions = _FakeCompletions() + client = SimpleNamespace(chat=SimpleNamespace(completions=completions)) + binding = EndpointBinding( + capability="vlm", + model="vision-model", + api_base="https://vlm.example/v1", + api_key="key", + configured=True, + request_kwargs={ + "max_tokens": configured_max_tokens, + "timeout_sec": 180, + }, + ) + + service = vlm_module.VLMService() + monkeypatch.setattr( + settings_module, + "get_runtime_settings", + lambda: SimpleNamespace(vlm_max_tokens=4096), + ) + monkeypatch.setattr(service, "_get_client", lambda _binding: (client, "vision-model")) + monkeypatch.setattr( + vlm_module, + "_encode_image", + lambda _path: ( + "aW1hZ2U=", + "image/jpeg", + {"source_bytes": 5, "payload_bytes": 5}, + ), + ) + + result = service.describe_image(image_path, binding) + + assert result["kind"] == "content" + assert result["text"] == "一张测试图片" + assert len(completions.calls) == 1 + assert completions.calls[0]["max_tokens"] == expected_max_tokens + assert completions.calls[0]["timeout"] == 180 diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..06fd87a --- /dev/null +++ b/uv.lock @@ -0,0 +1,3675 @@ +version = 1 +revision = 3 +requires-python = ">=3.11, <3.13" +resolution-markers = [ + "python_full_version >= '3.12' and platform_machine != 's390x'", + "python_full_version >= '3.12' and platform_machine == 's390x'", + "python_full_version < '3.12' and platform_machine != 's390x'", + "python_full_version < '3.12' and platform_machine == 's390x'", +] + +[[package]] +name = "absl-py" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/10/2a/c93173ffa1b39c1d0395b7e842bbdc62e556ca9d8d3b5572926f3e4ca752/absl_py-2.3.1.tar.gz", hash = "sha256:a97820526f7fbfd2ec1bce83f3f25e3a14840dac0d8e02a0b71cd75db3f77fc9", size = 116588, upload-time = "2025-07-03T09:31:44.05Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/aa/ba0014cc4659328dc818a28827be78e6d97312ab0cb98105a770924dc11e/absl_py-2.3.1-py3-none-any.whl", hash = "sha256:eeecf07f0c2a93ace0772c92e596ace6d3d3996c042b2128459aaae2a76de11d", size = 135811, upload-time = "2025-07-03T09:31:42.253Z" }, +] + +[[package]] +name = "accelerate" +version = "1.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyyaml" }, + { name = "safetensors" }, + { name = "torch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4a/8e/ac2a9566747a93f8be36ee08532eb0160558b07630a081a6056a9f89bf1d/accelerate-1.12.0.tar.gz", hash = "sha256:70988c352feb481887077d2ab845125024b2a137a5090d6d7a32b57d03a45df6", size = 398399, upload-time = "2025-11-21T11:27:46.973Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/d2/c581486aa6c4fbd7394c23c47b83fa1a919d34194e16944241daf9e762dd/accelerate-1.12.0-py3-none-any.whl", hash = "sha256:3e2091cd341423207e2f084a6654b1efcd250dc326f2a37d6dde446e07cabb11", size = 380935, upload-time = "2025-11-21T11:27:44.522Z" }, +] + +[[package]] +name = "aiofiles" +version = "24.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247, upload-time = "2024-06-24T11:02:03.584Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896, upload-time = "2024-06-24T11:02:01.529Z" }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/4a/064321452809dae953c1ed6e017504e72551a26b6f5708a5a80e4bf556ff/aiohttp-3.13.4.tar.gz", hash = "sha256:d97a6d09c66087890c2ab5d49069e1e570583f7ac0314ecf98294c1b6aaebd38", size = 7859748, upload-time = "2026-03-28T17:19:40.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/7e/cb94129302d78c46662b47f9897d642fd0b33bdfef4b73b20c6ced35aa4c/aiohttp-3.13.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8ea0c64d1bcbf201b285c2246c51a0c035ba3bbd306640007bc5844a3b4658c1", size = 760027, upload-time = "2026-03-28T17:15:33.022Z" }, + { url = "https://files.pythonhosted.org/packages/5e/cd/2db3c9397c3bd24216b203dd739945b04f8b87bb036c640da7ddb63c75ef/aiohttp-3.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6f742e1fa45c0ed522b00ede565e18f97e4cf8d1883a712ac42d0339dfb0cce7", size = 508325, upload-time = "2026-03-28T17:15:34.714Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/d28b2722ec13107f2e37a86b8a169897308bab6a3b9e071ecead9d67bd9b/aiohttp-3.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dcfb50ee25b3b7a1222a9123be1f9f89e56e67636b561441f0b304e25aaef8f", size = 502402, upload-time = "2026-03-28T17:15:36.409Z" }, + { url = "https://files.pythonhosted.org/packages/fa/d6/acd47b5f17c4430e555590990a4746efbcb2079909bb865516892bf85f37/aiohttp-3.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3262386c4ff370849863ea93b9ea60fd59c6cf56bf8f93beac625cf4d677c04d", size = 1771224, upload-time = "2026-03-28T17:15:38.223Z" }, + { url = "https://files.pythonhosted.org/packages/98/af/af6e20113ba6a48fd1cd9e5832c4851e7613ef50c7619acdaee6ec5f1aff/aiohttp-3.13.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:473bb5aa4218dd254e9ae4834f20e31f5a0083064ac0136a01a62ddbae2eaa42", size = 1731530, upload-time = "2026-03-28T17:15:39.988Z" }, + { url = "https://files.pythonhosted.org/packages/81/16/78a2f5d9c124ad05d5ce59a9af94214b6466c3491a25fb70760e98e9f762/aiohttp-3.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e56423766399b4c77b965f6aaab6c9546617b8994a956821cc507d00b91d978c", size = 1827925, upload-time = "2026-03-28T17:15:41.944Z" }, + { url = "https://files.pythonhosted.org/packages/2a/1f/79acf0974ced805e0e70027389fccbb7d728e6f30fcac725fb1071e63075/aiohttp-3.13.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8af249343fafd5ad90366a16d230fc265cf1149f26075dc9fe93cfd7c7173942", size = 1923579, upload-time = "2026-03-28T17:15:44.071Z" }, + { url = "https://files.pythonhosted.org/packages/af/53/29f9e2054ea6900413f3b4c3eb9d8331f60678ec855f13ba8714c47fd48d/aiohttp-3.13.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bc0a5cf4f10ef5a2c94fdde488734b582a3a7a000b131263e27c9295bd682d9", size = 1767655, upload-time = "2026-03-28T17:15:45.911Z" }, + { url = "https://files.pythonhosted.org/packages/f3/57/462fe1d3da08109ba4aa8590e7aed57c059af2a7e80ec21f4bac5cfe1094/aiohttp-3.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5c7ff1028e3c9fc5123a865ce17df1cb6424d180c503b8517afbe89aa566e6be", size = 1630439, upload-time = "2026-03-28T17:15:48.11Z" }, + { url = "https://files.pythonhosted.org/packages/d7/4b/4813344aacdb8127263e3eec343d24e973421143826364fa9fc847f6283f/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ba5cf98b5dcb9bddd857da6713a503fa6d341043258ca823f0f5ab7ab4a94ee8", size = 1745557, upload-time = "2026-03-28T17:15:50.13Z" }, + { url = "https://files.pythonhosted.org/packages/d4/01/1ef1adae1454341ec50a789f03cfafe4c4ac9c003f6a64515ecd32fe4210/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d85965d3ba21ee4999e83e992fecb86c4614d6920e40705501c0a1f80a583c12", size = 1741796, upload-time = "2026-03-28T17:15:52.351Z" }, + { url = "https://files.pythonhosted.org/packages/22/04/8cdd99af988d2aa6922714d957d21383c559835cbd43fbf5a47ddf2e0f05/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:49f0b18a9b05d79f6f37ddd567695943fcefb834ef480f17a4211987302b2dc7", size = 1805312, upload-time = "2026-03-28T17:15:54.407Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/b48d5577338d4b25bbdbae35c75dbfd0493cb8886dc586fbfb2e90862239/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7f78cb080c86fbf765920e5f1ef35af3f24ec4314d6675d0a21eaf41f6f2679c", size = 1621751, upload-time = "2026-03-28T17:15:56.564Z" }, + { url = "https://files.pythonhosted.org/packages/bc/89/4eecad8c1858e6d0893c05929e22343e0ebe3aec29a8a399c65c3cc38311/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:67a3ec705534a614b68bbf1c70efa777a21c3da3895d1c44510a41f5a7ae0453", size = 1826073, upload-time = "2026-03-28T17:15:58.489Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5c/9dc8293ed31b46c39c9c513ac7ca152b3c3d38e0ea111a530ad12001b827/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d6630ec917e85c5356b2295744c8a97d40f007f96a1c76bf1928dc2e27465393", size = 1760083, upload-time = "2026-03-28T17:16:00.677Z" }, + { url = "https://files.pythonhosted.org/packages/1e/19/8bbf6a4994205d96831f97b7d21a0feed120136e6267b5b22d229c6dc4dc/aiohttp-3.13.4-cp311-cp311-win32.whl", hash = "sha256:54049021bc626f53a5394c29e8c444f726ee5a14b6e89e0ad118315b1f90f5e3", size = 439690, upload-time = "2026-03-28T17:16:02.902Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f5/ac409ecd1007528d15c3e8c3a57d34f334c70d76cfb7128a28cffdebd4c1/aiohttp-3.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:c033f2bc964156030772d31cbf7e5defea181238ce1f87b9455b786de7d30145", size = 463824, upload-time = "2026-03-28T17:16:05.058Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bd/ede278648914cabbabfdf95e436679b5d4156e417896a9b9f4587169e376/aiohttp-3.13.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ee62d4471ce86b108b19c3364db4b91180d13fe3510144872d6bad5401957360", size = 752158, upload-time = "2026-03-28T17:16:06.901Z" }, + { url = "https://files.pythonhosted.org/packages/90/de/581c053253c07b480b03785196ca5335e3c606a37dc73e95f6527f1591fe/aiohttp-3.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c0fd8f41b54b58636402eb493afd512c23580456f022c1ba2db0f810c959ed0d", size = 501037, upload-time = "2026-03-28T17:16:08.82Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f9/a5ede193c08f13cc42c0a5b50d1e246ecee9115e4cf6e900d8dbd8fd6acb/aiohttp-3.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4baa48ce49efd82d6b1a0be12d6a36b35e5594d1dd42f8bfba96ea9f8678b88c", size = 501556, upload-time = "2026-03-28T17:16:10.63Z" }, + { url = "https://files.pythonhosted.org/packages/d6/10/88ff67cd48a6ec36335b63a640abe86135791544863e0cfe1f065d6cef7a/aiohttp-3.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d738ebab9f71ee652d9dbd0211057690022201b11197f9a7324fd4dba128aa97", size = 1757314, upload-time = "2026-03-28T17:16:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/8b/15/fdb90a5cf5a1f52845c276e76298c75fbbcc0ac2b4a86551906d54529965/aiohttp-3.13.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0ce692c3468fa831af7dceed52edf51ac348cebfc8d3feb935927b63bd3e8576", size = 1731819, upload-time = "2026-03-28T17:16:14.558Z" }, + { url = "https://files.pythonhosted.org/packages/ec/df/28146785a007f7820416be05d4f28cc207493efd1e8c6c1068e9bdc29198/aiohttp-3.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8e08abcfe752a454d2cb89ff0c08f2d1ecd057ae3e8cc6d84638de853530ebab", size = 1793279, upload-time = "2026-03-28T17:16:16.594Z" }, + { url = "https://files.pythonhosted.org/packages/10/47/689c743abf62ea7a77774d5722f220e2c912a77d65d368b884d9779ef41b/aiohttp-3.13.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5977f701b3fff36367a11087f30ea73c212e686d41cd363c50c022d48b011d8d", size = 1891082, upload-time = "2026-03-28T17:16:18.71Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/f7f4f318c7e58c23b761c9b13b9a3c9b394e0f9d5d76fbc6622fa98509f6/aiohttp-3.13.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54203e10405c06f8b6020bd1e076ae0fe6c194adcee12a5a78af3ffa3c57025e", size = 1773938, upload-time = "2026-03-28T17:16:21.125Z" }, + { url = "https://files.pythonhosted.org/packages/aa/06/f207cb3121852c989586a6fc16ff854c4fcc8651b86c5d3bd1fc83057650/aiohttp-3.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:358a6af0145bc4dda037f13167bef3cce54b132087acc4c295c739d05d16b1c3", size = 1579548, upload-time = "2026-03-28T17:16:23.588Z" }, + { url = "https://files.pythonhosted.org/packages/6c/58/e1289661a32161e24c1fe479711d783067210d266842523752869cc1d9c2/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:898ea1850656d7d61832ef06aa9846ab3ddb1621b74f46de78fbc5e1a586ba83", size = 1714669, upload-time = "2026-03-28T17:16:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/96/0a/3e86d039438a74a86e6a948a9119b22540bae037d6ba317a042ae3c22711/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7bc30cceb710cf6a44e9617e43eebb6e3e43ad855a34da7b4b6a73537d8a6763", size = 1754175, upload-time = "2026-03-28T17:16:28.18Z" }, + { url = "https://files.pythonhosted.org/packages/f4/30/e717fc5df83133ba467a560b6d8ef20197037b4bb5d7075b90037de1018e/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4a31c0c587a8a038f19a4c7e60654a6c899c9de9174593a13e7cc6e15ff271f9", size = 1762049, upload-time = "2026-03-28T17:16:30.941Z" }, + { url = "https://files.pythonhosted.org/packages/e4/28/8f7a2d4492e336e40005151bdd94baf344880a4707573378579f833a64c1/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2062f675f3fe6e06d6113eb74a157fb9df58953ffed0cdb4182554b116545758", size = 1570861, upload-time = "2026-03-28T17:16:32.953Z" }, + { url = "https://files.pythonhosted.org/packages/78/45/12e1a3d0645968b1c38de4b23fdf270b8637735ea057d4f84482ff918ad9/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d1ba8afb847ff80626d5e408c1fdc99f942acc877d0702fe137015903a220a9", size = 1790003, upload-time = "2026-03-28T17:16:35.468Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0f/60374e18d590de16dcb39d6ff62f39c096c1b958e6f37727b5870026ea30/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b08149419994cdd4d5eecf7fd4bc5986b5a9380285bcd01ab4c0d6bfca47b79d", size = 1737289, upload-time = "2026-03-28T17:16:38.187Z" }, + { url = "https://files.pythonhosted.org/packages/02/bf/535e58d886cfbc40a8b0013c974afad24ef7632d645bca0b678b70033a60/aiohttp-3.13.4-cp312-cp312-win32.whl", hash = "sha256:fc432f6a2c4f720180959bc19aa37259651c1a4ed8af8afc84dd41c60f15f791", size = 434185, upload-time = "2026-03-28T17:16:40.735Z" }, + { url = "https://files.pythonhosted.org/packages/1e/1a/d92e3325134ebfff6f4069f270d3aac770d63320bd1fcd0eca023e74d9a8/aiohttp-3.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:6148c9ae97a3e8bff9a1fc9c757fa164116f86c100468339730e717590a3fb77", size = 461285, upload-time = "2026-03-28T17:16:42.713Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "alembic" +version = "1.18.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mako" }, + { name = "sqlalchemy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/cc/aca263693b2ece99fa99a09b6d092acb89973eb2bb575faef1777e04f8b4/alembic-1.18.1.tar.gz", hash = "sha256:83ac6b81359596816fb3b893099841a0862f2117b2963258e965d70dc62fb866", size = 2044319, upload-time = "2026-01-14T18:53:14.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/36/cd9cb6101e81e39076b2fbe303bfa3c85ca34e55142b0324fcbf22c5c6e2/alembic-1.18.1-py3-none-any.whl", hash = "sha256:f1c3b0920b87134e851c25f1f7f236d8a332c34b75416802d06971df5d1b7810", size = 260973, upload-time = "2026-01-14T18:53:17.533Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "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.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "asteroid-filterbanks" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "torch" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/fa/5c2be1f96dc179f83cdd3bb267edbd1f47d08f756785c016d5c2163901a7/asteroid-filterbanks-0.4.0.tar.gz", hash = "sha256:415f89d1dcf2b13b35f03f7a9370968ac4e6fa6800633c522dac992b283409b9", size = 24599, upload-time = "2021-04-09T20:03:07.456Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/7c/83ff6046176a675e6a1e8aeefed8892cd97fe7c46af93cc540d1b24b8323/asteroid_filterbanks-0.4.0-py3-none-any.whl", hash = "sha256:4932ac8b6acc6e08fb87cbe8ece84215b5a74eee284fe83acf3540a72a02eaf5", size = 29912, upload-time = "2021-04-09T20:03:05.817Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "audio-separator" +version = "0.41.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beartype" }, + { name = "diffq", marker = "sys_platform != 'win32'" }, + { name = "diffq-fixed", marker = "sys_platform == 'win32'" }, + { name = "einops" }, + { name = "julius" }, + { name = "librosa" }, + { name = "ml-collections" }, + { name = "numpy" }, + { name = "onnx-weekly" }, + { name = "onnx2torch-py313" }, + { name = "pydub" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "resampy" }, + { name = "rotary-embedding-torch" }, + { name = "samplerate" }, + { name = "scipy" }, + { name = "six" }, + { name = "soundfile" }, + { name = "torch" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/97/ffaf6a0c4c2585da27e305eef43bc4979b23ccfe6ac1e631fa17ce78dc14/audio_separator-0.41.0.tar.gz", hash = "sha256:d4e44aa798b4bcb2747c5428bf4c8f5f54982a80ddc785b81c9b0e6a2f573224", size = 322439, upload-time = "2026-01-16T02:55:52.838Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/ae/0bdf0bf4ec4f58e14b1742528cf498b8c46abb4fa66c8e5359cceafc1505/audio_separator-0.41.0-py3-none-any.whl", hash = "sha256:df58c7d1718e5a80e897fae551d423e52221b1cf2bcd7d70cb8fc2878db3c6d0", size = 389224, upload-time = "2026-01-16T02:55:50.886Z" }, +] + +[package.optional-dependencies] +gpu = [ + { name = "onnxruntime-gpu" }, +] + +[[package]] +name = "audioread" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +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 = "av" +version = "15.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/c3/83e6e73d1592bc54436eae0bc61704ae0cff0c3cfbde7b58af9ed67ebb49/av-15.1.0.tar.gz", hash = "sha256:39cda2dc810e11c1938f8cb5759c41d6b630550236b3365790e67a313660ec85", size = 3774192, upload-time = "2025-08-30T04:41:56.076Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/58/4e44cf6939be7aba96a4abce024e1be11ba7539ecac74d09369b8c03aa05/av-15.1.0-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:b785948762a8d45fc58fc24a20251496829ace1817e9a7a508a348d6de2182c3", size = 21767323, upload-time = "2025-08-30T04:39:37.989Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f6/a946544cdb49f6d892d2761b1d61a8bc6ce912fe57ba06769bdc640c0a7f/av-15.1.0-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:9c7131494a3a318612b4ee4db98fe5bc50eb705f6b6536127c7ab776c524fd8b", size = 26946268, upload-time = "2025-08-30T04:39:40.601Z" }, + { url = "https://files.pythonhosted.org/packages/70/7c/b33513c0af73d0033af59a98f035b521c5b93445a6af7e9efbf41a6e8383/av-15.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:2b9623ae848625c59213b610c8665817924f913580c7c5c91e0dc18936deb00d", size = 38062118, upload-time = "2025-08-30T04:39:43.928Z" }, + { url = "https://files.pythonhosted.org/packages/5e/95/31b7fb34f9fea7c7389240364194f4f56ad2d460095038cc720f50a90bb3/av-15.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:c8ef597087db560514617143532b1fafc4825ebb2dda9a22418f548b113a0cc7", size = 39571086, upload-time = "2025-08-30T04:39:47.109Z" }, + { url = "https://files.pythonhosted.org/packages/e7/b0/7b0b45474a4e90c35c11d0032947d8b3c7386872957ce29c6f12add69a74/av-15.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:08eac47a90ebae1e2bd5935f400dd515166019bab4ff5b03c4625fa6ac3a0a5e", size = 40112634, upload-time = "2025-08-30T04:39:50.981Z" }, + { url = "https://files.pythonhosted.org/packages/aa/04/038b94bc9a1ee10a451c867d4a2fc91e845f83bfc2dae9df25893abcb57f/av-15.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d3f66ff200ea166e606cb3c5cb1bd2fc714effbec2e262a5d67ce60450c8234a", size = 40878695, upload-time = "2025-08-30T04:39:54.493Z" }, + { url = "https://files.pythonhosted.org/packages/1d/3d/9f8f96c0deeaaf648485a3dbd1699b2f0580f2ce8a36cb616c0138ba7615/av-15.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:57b99544d91121b8bea570e4ddf61700f679a6b677c1f37966bc1a22e1d4cd5c", size = 31335683, upload-time = "2025-08-30T04:39:57.861Z" }, + { url = "https://files.pythonhosted.org/packages/d1/58/de78b276d20db6ffcd4371283df771721a833ba525a3d57e753d00a9fe79/av-15.1.0-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:40c5df37f4c354ab8190c6fd68dab7881d112f527906f64ca73da4c252a58cee", size = 21760991, upload-time = "2025-08-30T04:40:00.801Z" }, + { url = "https://files.pythonhosted.org/packages/56/cc/45f85775304ae60b66976360d82ba5b152ad3fd91f9267d5020a51e9a828/av-15.1.0-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:af455ce65ada3d361f80c90c810d9bced4db5655ab9aa513024d6c71c5c476d5", size = 26953097, upload-time = "2025-08-30T04:40:03.998Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f8/2d781e5e71d02fc829487e775ccb1185e72f95340d05f2e84eb57a11e093/av-15.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:86226d2474c80c3393fa07a9c366106029ae500716098b72b3ec3f67205524c3", size = 38319710, upload-time = "2025-08-30T04:40:07.701Z" }, + { url = "https://files.pythonhosted.org/packages/ac/13/37737ef2193e83862ccacff23580c39de251da456a1bf0459e762cca273c/av-15.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:11326f197e7001c4ca53a83b2dbc67fd39ddff8cdf62ce6be3b22d9f3f9338bd", size = 39915519, upload-time = "2025-08-30T04:40:11.066Z" }, + { url = "https://files.pythonhosted.org/packages/26/e9/e8032c7b8f2a4129a03f63f896544f8b7cf068e2db2950326fa2400d5c47/av-15.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a631ea879cc553080ee62874f4284765c42ba08ee0279851a98a85e2ceb3cc8d", size = 40286166, upload-time = "2025-08-30T04:40:14.561Z" }, + { url = "https://files.pythonhosted.org/packages/e2/23/612c0fd809444d04b8387a2dfd942ccc77829507bd78a387ff65a9d98c24/av-15.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8f383949b010c3e731c245f80351d19dc0c08f345e194fc46becb1cb279be3ff", size = 41150592, upload-time = "2025-08-30T04:40:17.951Z" }, + { url = "https://files.pythonhosted.org/packages/15/74/6f8e38a3b0aea5f28e72813672ff45b64615f2c69e6a4a558718c95edb9f/av-15.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:d5921aa45f4c1f8c1a8d8185eb347e02aa4c3071278a2e2dd56368d54433d643", size = 31336093, upload-time = "2025-08-30T04:40:21.393Z" }, +] + +[[package]] +name = "beartype" +version = "0.18.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/15/4e623478a9628ad4cee2391f19aba0b16c1dd6fedcb2a399f0928097b597/beartype-0.18.5.tar.gz", hash = "sha256:264ddc2f1da9ec94ff639141fbe33d22e12a9f75aa863b83b7046ffff1381927", size = 1193506, upload-time = "2024-04-21T07:25:58.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/43/7a1259741bd989723272ac7d381a43be932422abcff09a1d9f7ba212cb74/beartype-0.18.5-py3-none-any.whl", hash = "sha256:5301a14f2a9a5540fe47ec6d34d758e9cd8331d36c4760fc7a5499ab86310089", size = 917762, upload-time = "2024-04-21T07:25:55.758Z" }, +] + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, +] + +[[package]] +name = "brotli" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/16/c92ca344d646e71a43b8bb353f0a6490d7f6e06210f8554c8f874e454285/brotli-1.2.0.tar.gz", hash = "sha256:e310f77e41941c13340a95976fe66a8a95b01e783d430eeaf7a2f87e0a57dd0a", size = 7388632, upload-time = "2025-11-05T18:39:42.86Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/ef/f285668811a9e1ddb47a18cb0b437d5fc2760d537a2fe8a57875ad6f8448/brotli-1.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:15b33fe93cedc4caaff8a0bd1eb7e3dab1c61bb22a0bf5bdfdfd97cd7da79744", size = 863110, upload-time = "2025-11-05T18:38:12.978Z" }, + { url = "https://files.pythonhosted.org/packages/50/62/a3b77593587010c789a9d6eaa527c79e0848b7b860402cc64bc0bc28a86c/brotli-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:898be2be399c221d2671d29eed26b6b2713a02c2119168ed914e7d00ceadb56f", size = 445438, upload-time = "2025-11-05T18:38:14.208Z" }, + { url = "https://files.pythonhosted.org/packages/cd/e1/7fadd47f40ce5549dc44493877db40292277db373da5053aff181656e16e/brotli-1.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:350c8348f0e76fff0a0fd6c26755d2653863279d086d3aa2c290a6a7251135dd", size = 1534420, upload-time = "2025-11-05T18:38:15.111Z" }, + { url = "https://files.pythonhosted.org/packages/12/8b/1ed2f64054a5a008a4ccd2f271dbba7a5fb1a3067a99f5ceadedd4c1d5a7/brotli-1.2.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e1ad3fda65ae0d93fec742a128d72e145c9c7a99ee2fcd667785d99eb25a7fe", size = 1632619, upload-time = "2025-11-05T18:38:16.094Z" }, + { url = "https://files.pythonhosted.org/packages/89/5a/7071a621eb2d052d64efd5da2ef55ecdac7c3b0c6e4f9d519e9c66d987ef/brotli-1.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:40d918bce2b427a0c4ba189df7a006ac0c7277c180aee4617d99e9ccaaf59e6a", size = 1426014, upload-time = "2025-11-05T18:38:17.177Z" }, + { url = "https://files.pythonhosted.org/packages/26/6d/0971a8ea435af5156acaaccec1a505f981c9c80227633851f2810abd252a/brotli-1.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2a7f1d03727130fc875448b65b127a9ec5d06d19d0148e7554384229706f9d1b", size = 1489661, upload-time = "2025-11-05T18:38:18.41Z" }, + { url = "https://files.pythonhosted.org/packages/f3/75/c1baca8b4ec6c96a03ef8230fab2a785e35297632f402ebb1e78a1e39116/brotli-1.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9c79f57faa25d97900bfb119480806d783fba83cd09ee0b33c17623935b05fa3", size = 1599150, upload-time = "2025-11-05T18:38:19.792Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1a/23fcfee1c324fd48a63d7ebf4bac3a4115bdb1b00e600f80f727d850b1ae/brotli-1.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:844a8ceb8483fefafc412f85c14f2aae2fb69567bf2a0de53cdb88b73e7c43ae", size = 1493505, upload-time = "2025-11-05T18:38:20.913Z" }, + { url = "https://files.pythonhosted.org/packages/36/e5/12904bbd36afeef53d45a84881a4810ae8810ad7e328a971ebbfd760a0b3/brotli-1.2.0-cp311-cp311-win32.whl", hash = "sha256:aa47441fa3026543513139cb8926a92a8e305ee9c71a6209ef7a97d91640ea03", size = 334451, upload-time = "2025-11-05T18:38:21.94Z" }, + { url = "https://files.pythonhosted.org/packages/02/8b/ecb5761b989629a4758c394b9301607a5880de61ee2ee5fe104b87149ebc/brotli-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:022426c9e99fd65d9475dce5c195526f04bb8be8907607e27e747893f6ee3e24", size = 369035, upload-time = "2025-11-05T18:38:22.941Z" }, + { url = "https://files.pythonhosted.org/packages/11/ee/b0a11ab2315c69bb9b45a2aaed022499c9c24a205c3a49c3513b541a7967/brotli-1.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:35d382625778834a7f3061b15423919aa03e4f5da34ac8e02c074e4b75ab4f84", size = 861543, upload-time = "2025-11-05T18:38:24.183Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2f/29c1459513cd35828e25531ebfcbf3e92a5e49f560b1777a9af7203eb46e/brotli-1.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a61c06b334bd99bc5ae84f1eeb36bfe01400264b3c352f968c6e30a10f9d08b", size = 444288, upload-time = "2025-11-05T18:38:25.139Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6f/feba03130d5fceadfa3a1bb102cb14650798c848b1df2a808356f939bb16/brotli-1.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acec55bb7c90f1dfc476126f9711a8e81c9af7fb617409a9ee2953115343f08d", size = 1528071, upload-time = "2025-11-05T18:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/2b/38/f3abb554eee089bd15471057ba85f47e53a44a462cfce265d9bf7088eb09/brotli-1.2.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:260d3692396e1895c5034f204f0db022c056f9e2ac841593a4cf9426e2a3faca", size = 1626913, upload-time = "2025-11-05T18:38:27.284Z" }, + { url = "https://files.pythonhosted.org/packages/03/a7/03aa61fbc3c5cbf99b44d158665f9b0dd3d8059be16c460208d9e385c837/brotli-1.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:072e7624b1fc4d601036ab3f4f27942ef772887e876beff0301d261210bca97f", size = 1419762, upload-time = "2025-11-05T18:38:28.295Z" }, + { url = "https://files.pythonhosted.org/packages/21/1b/0374a89ee27d152a5069c356c96b93afd1b94eae83f1e004b57eb6ce2f10/brotli-1.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adedc4a67e15327dfdd04884873c6d5a01d3e3b6f61406f99b1ed4865a2f6d28", size = 1484494, upload-time = "2025-11-05T18:38:29.29Z" }, + { url = "https://files.pythonhosted.org/packages/cf/57/69d4fe84a67aef4f524dcd075c6eee868d7850e85bf01d778a857d8dbe0a/brotli-1.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7a47ce5c2288702e09dc22a44d0ee6152f2c7eda97b3c8482d826a1f3cfc7da7", size = 1593302, upload-time = "2025-11-05T18:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/d5/3b/39e13ce78a8e9a621c5df3aeb5fd181fcc8caba8c48a194cd629771f6828/brotli-1.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:af43b8711a8264bb4e7d6d9a6d004c3a2019c04c01127a868709ec29962b6036", size = 1487913, upload-time = "2025-11-05T18:38:31.618Z" }, + { url = "https://files.pythonhosted.org/packages/62/28/4d00cb9bd76a6357a66fcd54b4b6d70288385584063f4b07884c1e7286ac/brotli-1.2.0-cp312-cp312-win32.whl", hash = "sha256:e99befa0b48f3cd293dafeacdd0d191804d105d279e0b387a32054c1180f3161", size = 334362, upload-time = "2025-11-05T18:38:32.939Z" }, + { url = "https://files.pythonhosted.org/packages/1c/4e/bc1dcac9498859d5e353c9b153627a3752868a9d5f05ce8dedd81a2354ab/brotli-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:b35c13ce241abdd44cb8ca70683f20c0c079728a36a996297adb5334adfc1c44", size = 369115, upload-time = "2025-11-05T18:38:33.765Z" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, +] + +[[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 = "coloredlogs" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "humanfriendly" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520, upload-time = "2021-06-11T10:22:45.202Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018, upload-time = "2021-06-11T10:22:42.561Z" }, +] + +[[package]] +name = "colorlog" +version = "6.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/61/f083b5ac52e505dfc1c624eafbf8c7589a0d7f32daa398d2e7590efa5fda/colorlog-6.10.1.tar.gz", hash = "sha256:eb4ae5cb65fe7fec7773c2306061a8e63e02efc2c72eba9d27b0fa23c94f1321", size = 17162, upload-time = "2025-10-16T16:14:11.978Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/c1/e419ef3723a074172b68aaa89c9f3de486ed4c2399e2dbd8113a4fdcaf9e/colorlog-6.10.1-py3-none-any.whl", hash = "sha256:2d7e8348291948af66122cff006c9f8da6255d224e7cf8e37d8de2df3bad8c9c", size = 11743, upload-time = "2025-10-16T16:14:10.512Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "cython" +version = "3.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/85/7574c9cd44b69a27210444b6650f6477f56c75fee1b70d7672d3e4166167/cython-3.2.4.tar.gz", hash = "sha256:84226ecd313b233da27dc2eb3601b4f222b8209c3a7216d8733b031da1dc64e6", size = 3280291, upload-time = "2026-01-04T14:14:14.473Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/cc/8f06145ec3efa121c8b1b67f06a640386ddacd77ee3e574da582a21b14ee/cython-3.2.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff9af2134c05e3734064808db95b4dd7341a39af06e8945d05ea358e1741aaed", size = 2953769, upload-time = "2026-01-04T14:15:00.361Z" }, + { url = "https://files.pythonhosted.org/packages/55/b0/706cf830eddd831666208af1b3058c2e0758ae157590909c1f634b53bed9/cython-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67922c9de058a0bfb72d2e75222c52d09395614108c68a76d9800f150296ddb3", size = 3243841, upload-time = "2026-01-04T14:15:02.066Z" }, + { url = "https://files.pythonhosted.org/packages/ac/25/58893afd4ef45f79e3d4db82742fa4ff874b936d67a83c92939053920ccd/cython-3.2.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b362819d155fff1482575e804e43e3a8825332d32baa15245f4642022664a3f4", size = 3378083, upload-time = "2026-01-04T14:15:04.248Z" }, + { url = "https://files.pythonhosted.org/packages/32/e4/424a004d7c0d8a4050c81846ebbd22272ececfa9a498cb340aa44fccbec2/cython-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:1a64a112a34ec719b47c01395647e54fb4cf088a511613f9a3a5196694e8e382", size = 2769990, upload-time = "2026-01-04T14:15:06.53Z" }, + { url = "https://files.pythonhosted.org/packages/91/4d/1eb0c7c196a136b1926f4d7f0492a96c6fabd604d77e6cd43b56a3a16d83/cython-3.2.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:64d7f71be3dd6d6d4a4c575bb3a4674ea06d1e1e5e4cd1b9882a2bc40ed3c4c9", size = 2970064, upload-time = "2026-01-04T14:15:08.567Z" }, + { url = "https://files.pythonhosted.org/packages/03/1c/46e34b08bea19a1cdd1e938a4c123e6299241074642db9d81983cef95e9f/cython-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:869487ea41d004f8b92171f42271fbfadb1ec03bede3158705d16cd570d6b891", size = 3226757, upload-time = "2026-01-04T14:15:10.812Z" }, + { url = "https://files.pythonhosted.org/packages/12/33/3298a44d201c45bcf0d769659725ae70e9c6c42adf8032f6d89c8241098d/cython-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:55b6c44cd30821f0b25220ceba6fe636ede48981d2a41b9bbfe3c7902ce44ea7", size = 3388969, upload-time = "2026-01-04T14:15:12.45Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f3/4275cd3ea0a4cf4606f9b92e7f8766478192010b95a7f516d1b7cf22cb10/cython-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:767b143704bdd08a563153448955935844e53b852e54afdc552b43902ed1e235", size = 2756457, upload-time = "2026-01-04T14:15:14.67Z" }, + { url = "https://files.pythonhosted.org/packages/0a/8b/fd393f0923c82be4ec0db712fffb2ff0a7a131707b842c99bf24b549274d/cython-3.2.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:36bf3f5eb56d5281aafabecbaa6ed288bc11db87547bba4e1e52943ae6961ccf", size = 2875622, upload-time = "2026-01-04T14:15:39.749Z" }, + { url = "https://files.pythonhosted.org/packages/73/48/48530d9b9d64ec11dbe0dd3178a5fe1e0b27977c1054ecffb82be81e9b6a/cython-3.2.4-cp39-abi3-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6d5267f22b6451eb1e2e1b88f6f78a2c9c8733a6ddefd4520d3968d26b824581", size = 3210669, upload-time = "2026-01-04T14:15:41.911Z" }, + { url = "https://files.pythonhosted.org/packages/5e/91/4865fbfef1f6bb4f21d79c46104a53d1a3fa4348286237e15eafb26e0828/cython-3.2.4-cp39-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3b6e58f73a69230218d5381817850ce6d0da5bb7e87eb7d528c7027cbba40b06", size = 2856835, upload-time = "2026-01-04T14:15:43.815Z" }, + { url = "https://files.pythonhosted.org/packages/fa/39/60317957dbef179572398253f29d28f75f94ab82d6d39ea3237fb6c89268/cython-3.2.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e71efb20048358a6b8ec604a0532961c50c067b5e63e345e2e359fff72feaee8", size = 2994408, upload-time = "2026-01-04T14:15:45.422Z" }, + { url = "https://files.pythonhosted.org/packages/8d/30/7c24d9292650db4abebce98abc9b49c820d40fa7c87921c0a84c32f4efe7/cython-3.2.4-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:28b1e363b024c4b8dcf52ff68125e635cb9cb4b0ba997d628f25e32543a71103", size = 2891478, upload-time = "2026-01-04T14:15:47.394Z" }, + { url = "https://files.pythonhosted.org/packages/86/70/03dc3c962cde9da37a93cca8360e576f904d5f9beecfc9d70b1f820d2e5f/cython-3.2.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:31a90b4a2c47bb6d56baeb926948348ec968e932c1ae2c53239164e3e8880ccf", size = 3225663, upload-time = "2026-01-04T14:15:49.446Z" }, + { url = "https://files.pythonhosted.org/packages/b1/97/10b50c38313c37b1300325e2e53f48ea9a2c078a85c0c9572057135e31d5/cython-3.2.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e65e4773021f8dc8532010b4fbebe782c77f9a0817e93886e518c93bd6a44e9d", size = 3115628, upload-time = "2026-01-04T14:15:51.323Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b1/d6a353c9b147848122a0db370863601fdf56de2d983b5c4a6a11e6ee3cd7/cython-3.2.4-cp39-abi3-win32.whl", hash = "sha256:2b1f12c0e4798293d2754e73cd6f35fa5bbdf072bdc14bc6fc442c059ef2d290", size = 2437463, upload-time = "2026-01-04T14:15:53.787Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d8/319a1263b9c33b71343adfd407e5daffd453daef47ebc7b642820a8b68ed/cython-3.2.4-cp39-abi3-win_arm64.whl", hash = "sha256:3b8e62049afef9da931d55de82d8f46c9a147313b69d5ff6af6e9121d545ce7a", size = 2442754, upload-time = "2026-01-04T14:15:55.382Z" }, + { url = "https://files.pythonhosted.org/packages/ff/fa/d3c15189f7c52aaefbaea76fb012119b04b9013f4bf446cb4eb4c26c4e6b/cython-3.2.4-py3-none-any.whl", hash = "sha256:732fc93bc33ae4b14f6afaca663b916c2fdd5dcbfad7114e17fb2434eeaea45c", size = 1257078, upload-time = "2026-01-04T14:14:12.373Z" }, +] + +[[package]] +name = "decorator" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + +[[package]] +name = "diffq" +version = "0.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cython" }, + { name = "numpy" }, + { name = "torch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/fd/4c58807bf855c5929ffa6da55f26dd6b9ae462a4193f5e09cc49fbbfd451/diffq-0.2.4.tar.gz", hash = "sha256:049064861e974ebf00d0badab8b324c775037371419eda3150985b9d477b5bd2", size = 157139, upload-time = "2023-05-05T12:39:43.089Z" } + +[[package]] +name = "diffq-fixed" +version = "0.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cython" }, + { name = "numpy" }, + { name = "torch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/96/8ca5acf5ecfd4108aa6f345cef171f2fda0c081cf0e7430671712586f172/diffq_fixed-0.2.4.tar.gz", hash = "sha256:cbc906b76fa23d1cf3c0ae517fbab744d9624980a068fe7fbb00dede1d83208d", size = 189695, upload-time = "2024-11-07T21:33:26.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/70/a11838773e1c2bd9fa47d5016a90b7584b4052179407fb934eb89499552e/diffq_fixed-0.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:67798dccc9b4f46196071953d52fafbef0751c5d0acb064e0883c7a8509d84f9", size = 106290, upload-time = "2024-11-12T18:22:14.423Z" }, + { url = "https://files.pythonhosted.org/packages/33/14/94327d99f551136ac95bdc9697a1738f78ab3bb63afe8ce58b2b9854cd3a/diffq_fixed-0.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:81087279979db570723ca0911265635d7c3132cc8b74bc52bdea94881440ce96", size = 106205, upload-time = "2024-11-07T21:33:24.767Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "docopt" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/55/8f8cab2afd404cf578136ef2cc5dfb50baa1761b68c9da1fb1e4eed343c9/docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491", size = 25901, upload-time = "2014-06-16T11:18:57.406Z" } + +[[package]] +name = "dynet38" +version = "2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cython" }, + { name = "numpy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/93/0c08e87f2616b40cce781f8654de5ed872fbe8c6a623dbfb794864756977/dyNET38-2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3f5ed1499c8e379fb6281d7d00169cb57c088846e71e66e4462ab355521b6148", size = 3383951, upload-time = "2024-01-23T10:31:45.752Z" }, + { url = "https://files.pythonhosted.org/packages/ee/5a/422aa4e06e81187be9f61d737a249edaac2e0d417ba7d1e2fab0cf107485/dyNET38-2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9a6b74b7e3e9e2f4cc253d25e35a46a1f0b8603a22d4c15ca2238e46401e1407", size = 2954588, upload-time = "2024-06-14T15:19:31.714Z" }, + { url = "https://files.pythonhosted.org/packages/8b/34/0cb9c8e3602ef3544c61e2b123e12d37f1b1ebed98d39b82aa627737bb6a/dyNET38-2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:313799701f7e44cbfaff98a88f1373beef9be57aa340e7e986238673de7d9173", size = 6510091, upload-time = "2024-06-16T11:36:25.655Z" }, + { url = "https://files.pythonhosted.org/packages/43/3c/4297be1b6b4e8c3534c88a49f4122edeac31be61206af0c87e1f6586d51e/dyNET38-2.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fdbb82e2f704e2b174b0567539553fda79424583be10e61e7c8e2c022afe9c0", size = 6499613, upload-time = "2024-01-23T10:31:48.668Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c3/46f879632262817aced0889c15c694d327173b3f75467994201d9ddc1f38/dyNET38-2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8963383d935e802941c8f9d778403798452e172150c3b4b5dee288d50b62ecc8", size = 6961879, upload-time = "2024-01-23T10:31:51.197Z" }, + { url = "https://files.pythonhosted.org/packages/76/da/73dd8148bfb041bad86dfba2349c5dca0a588bbba4dd9b7f8af8c1558dfd/dyNET38-2.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:81c8bd50e7bf9a9a8749d5e3876fc95fb8072a00ec98bf00e7346de59f9d24d8", size = 7066935, upload-time = "2024-01-23T10:31:54.438Z" }, + { url = "https://files.pythonhosted.org/packages/47/6f/c252d1c450d173e0acca83a9f4edbfa2921ea133f349410fb87ff9c146b8/dyNET38-2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bb54f56deae8160b7984ade15d377ac655576ea98959b8e10f937ace2d108617", size = 7486721, upload-time = "2024-01-23T10:31:56.726Z" }, + { url = "https://files.pythonhosted.org/packages/4e/49/1b30c6d908ac16a13bde7ee809dd03f297718e641d55fc995f1cce9c99d5/dyNET38-2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5975e52b4f7a933321af61647b98ae1e1dead156b15e05f139c7b71356e27e96", size = 7140574, upload-time = "2024-06-16T11:36:28.034Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ab/9b90acd5b77c7ea64dcf7c258b9fa20e8229a00cefbe41b855d805c67384/dyNET38-2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5e6a644c0ffc68083d85b67aaa639abe82812f384962ec074749bd68bcd3d9e3", size = 2947396, upload-time = "2024-06-14T15:19:39.498Z" }, + { url = "https://files.pythonhosted.org/packages/0f/4b/bbc70a71ce666e204f1086e315df6e9094dd6acbc17f55fa6d467e26fce2/dyNET38-2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54be808a97fd3cc36476f0d5a932f021739a5c28989f6e217320655c13467978", size = 6252937, upload-time = "2024-06-16T11:36:30.895Z" }, + { url = "https://files.pythonhosted.org/packages/85/14/33e2beb5be4c1298c339c1b1146dca149b09cd047dd0a6eb68957f3c465a/dyNET38-2.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:60a84845b0043c6cbd4db6f364a97b282fdb0953461ae9ee9e17a6e8433de7bb", size = 6215636, upload-time = "2024-01-23T10:31:59.784Z" }, + { url = "https://files.pythonhosted.org/packages/94/da/da6f1e5bcda0cfbadcb1fd1ef8c7435e36b7cc36e57368decb80ac54531f/dyNET38-2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8693bf8e98fdddfa116fa1e213f1c471be772e1b9e006ad5278a2b2a8f7b4d70", size = 6748542, upload-time = "2024-01-23T10:32:03.414Z" }, + { url = "https://files.pythonhosted.org/packages/ad/d5/0617734c5b4fe09d15f548f43a0d12da9dcd276e0c740f380270f57b2889/dyNET38-2.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cd7c30a32805ebed012746603bd3b3a1194b13aa0efc828d4e0035e20e8d5ee0", size = 6741756, upload-time = "2024-01-23T10:32:06.43Z" }, + { url = "https://files.pythonhosted.org/packages/82/1b/d83607f75f5a71d891f2408bdca31eaa166fb43dda2f792625f75bb0d275/dyNET38-2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:573d6d2cccdfee6f93ca4a3a2e4bc12f0e76c75b91d1eeb420546bec28c0b4b0", size = 7246792, upload-time = "2024-01-23T10:32:08.827Z" }, + { url = "https://files.pythonhosted.org/packages/20/2e/25a78ba8dd9bc045811f78d656aba145900e1393bae1bfc1c6f93dbabb76/dyNET38-2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:74d82985200bb8a557cbafc0865e16160bb34682b70608473b5cd88e56bd72ba", size = 7028081, upload-time = "2024-06-16T11:36:33.797Z" }, + { url = "https://files.pythonhosted.org/packages/0e/50/0f9c7c702baabb6f477c124415a4c7cffb02e9c4891a9ed101a20e332733/dynet38-2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fed13baeb332b9d761ffd3e61983e4b0a107ab57e0e5fbde90e3f94d059306c8", size = 3695298, upload-time = "2025-12-30T19:08:20.554Z" }, + { url = "https://files.pythonhosted.org/packages/51/c0/be3c5934bba18fa0fa92e183158423687464e43c484d04fb51dfde3b9bf6/dynet38-2.2-cp311-cp311-win_amd64.whl", hash = "sha256:3d5a88d2ea7b518b422d57b3fbc123cd7f42e78c80357736f6f7b8f179fac6d9", size = 1306616, upload-time = "2025-12-29T12:14:42.027Z" }, + { url = "https://files.pythonhosted.org/packages/69/35/dc76f2a745762caffa2326a50c2cf4eea06279f2fa03927e0d93f9c36e5a/dynet38-2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:772c053aca8956d7e0ba5f463ed52435800482fb8e3c8cbb411cdffa8a9e90a2", size = 3643925, upload-time = "2025-12-30T19:08:24.37Z" }, + { url = "https://files.pythonhosted.org/packages/ff/4f/a5b22e11549f0a5dc6feccdb50414aaf3c05decf0d8ca29288984a21fc5a/dynet38-2.2-cp312-cp312-win_amd64.whl", hash = "sha256:fd9967ed3ec071cc4c0c084d758ab44dac34cb37beb625ea9fcf2221c2085975", size = 1299414, upload-time = "2025-12-29T12:14:44Z" }, +] + +[[package]] +name = "einops" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/81/df4fbe24dff8ba3934af99044188e20a98ed441ad17a274539b74e82e126/einops-0.8.1.tar.gz", hash = "sha256:de5d960a7a761225532e0f1959e5315ebeafc0cd43394732f103ca44b9837e84", size = 54805, upload-time = "2025-02-09T03:17:00.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/62/9773de14fe6c45c23649e98b83231fffd7b9892b6cf863251dc2afa73643/einops-0.8.1-py3-none-any.whl", hash = "sha256:919387eb55330f5757c6bea9165c5ff5cfe63a642682ea788a6d472576d81737", size = 64359, upload-time = "2025-02-09T03:17:01.998Z" }, +] + +[[package]] +name = "fastapi" +version = "0.128.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/08/8c8508db6c7b9aae8f7175046af41baad690771c9bcde676419965e338c7/fastapi-0.128.0.tar.gz", hash = "sha256:1cc179e1cef10a6be60ffe429f79b829dce99d8de32d7acb7e6c8dfdf7f2645a", size = 365682, upload-time = "2025-12-27T15:21:13.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094, upload-time = "2025-12-27T15:21:12.154Z" }, +] + +[[package]] +name = "fastuuid" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/7d/d9daedf0f2ebcacd20d599928f8913e9d2aea1d56d2d355a93bfa2b611d7/fastuuid-0.14.0.tar.gz", hash = "sha256:178947fc2f995b38497a74172adee64fdeb8b7ec18f2a5934d037641ba265d26", size = 18232, upload-time = "2025-10-19T22:19:22.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/f3/12481bda4e5b6d3e698fbf525df4443cc7dce746f246b86b6fcb2fba1844/fastuuid-0.14.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:73946cb950c8caf65127d4e9a325e2b6be0442a224fd51ba3b6ac44e1912ce34", size = 516386, upload-time = "2025-10-19T22:42:40.176Z" }, + { url = "https://files.pythonhosted.org/packages/59/19/2fc58a1446e4d72b655648eb0879b04e88ed6fa70d474efcf550f640f6ec/fastuuid-0.14.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:12ac85024637586a5b69645e7ed986f7535106ed3013640a393a03e461740cb7", size = 264569, upload-time = "2025-10-19T22:25:50.977Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/3c74756e5b02c40cfcc8b1d8b5bac4edbd532b55917a6bcc9113550e99d1/fastuuid-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:05a8dde1f395e0c9b4be515b7a521403d1e8349443e7641761af07c7ad1624b1", size = 254366, upload-time = "2025-10-19T22:29:49.166Z" }, + { url = "https://files.pythonhosted.org/packages/52/96/d761da3fccfa84f0f353ce6e3eb8b7f76b3aa21fd25e1b00a19f9c80a063/fastuuid-0.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09378a05020e3e4883dfdab438926f31fea15fd17604908f3d39cbeb22a0b4dc", size = 278978, upload-time = "2025-10-19T22:35:41.306Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c2/f84c90167cc7765cb82b3ff7808057608b21c14a38531845d933a4637307/fastuuid-0.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbb0c4b15d66b435d2538f3827f05e44e2baafcc003dd7d8472dc67807ab8fd8", size = 279692, upload-time = "2025-10-19T22:25:36.997Z" }, + { url = "https://files.pythonhosted.org/packages/af/7b/4bacd03897b88c12348e7bd77943bac32ccf80ff98100598fcff74f75f2e/fastuuid-0.14.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cd5a7f648d4365b41dbf0e38fe8da4884e57bed4e77c83598e076ac0c93995e7", size = 303384, upload-time = "2025-10-19T22:29:46.578Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a2/584f2c29641df8bd810d00c1f21d408c12e9ad0c0dafdb8b7b29e5ddf787/fastuuid-0.14.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c0a94245afae4d7af8c43b3159d5e3934c53f47140be0be624b96acd672ceb73", size = 460921, upload-time = "2025-10-19T22:36:42.006Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/c6b77443bb7764c760e211002c8638c0c7cce11cb584927e723215ba1398/fastuuid-0.14.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:2b29e23c97e77c3a9514d70ce343571e469098ac7f5a269320a0f0b3e193ab36", size = 480575, upload-time = "2025-10-19T22:28:18.975Z" }, + { url = "https://files.pythonhosted.org/packages/5a/87/93f553111b33f9bb83145be12868c3c475bf8ea87c107063d01377cc0e8e/fastuuid-0.14.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1e690d48f923c253f28151b3a6b4e335f2b06bf669c68a02665bc150b7839e94", size = 452317, upload-time = "2025-10-19T22:25:32.75Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8c/a04d486ca55b5abb7eaa65b39df8d891b7b1635b22db2163734dc273579a/fastuuid-0.14.0-cp311-cp311-win32.whl", hash = "sha256:a6f46790d59ab38c6aa0e35c681c0484b50dc0acf9e2679c005d61e019313c24", size = 154804, upload-time = "2025-10-19T22:24:15.615Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b2/2d40bf00820de94b9280366a122cbaa60090c8cf59e89ac3938cf5d75895/fastuuid-0.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:e150eab56c95dc9e3fefc234a0eedb342fac433dacc273cd4d150a5b0871e1fa", size = 156099, upload-time = "2025-10-19T22:24:31.646Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/e78fcc5df65467f0d207661b7ef86c5b7ac62eea337c0c0fcedbeee6fb13/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77e94728324b63660ebf8adb27055e92d2e4611645bf12ed9d88d30486471d0a", size = 510164, upload-time = "2025-10-19T22:31:45.635Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b3/c846f933f22f581f558ee63f81f29fa924acd971ce903dab1a9b6701816e/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:caa1f14d2102cb8d353096bc6ef6c13b2c81f347e6ab9d6fbd48b9dea41c153d", size = 261837, upload-time = "2025-10-19T22:38:38.53Z" }, + { url = "https://files.pythonhosted.org/packages/54/ea/682551030f8c4fa9a769d9825570ad28c0c71e30cf34020b85c1f7ee7382/fastuuid-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d23ef06f9e67163be38cece704170486715b177f6baae338110983f99a72c070", size = 251370, upload-time = "2025-10-19T22:40:26.07Z" }, + { url = "https://files.pythonhosted.org/packages/14/dd/5927f0a523d8e6a76b70968e6004966ee7df30322f5fc9b6cdfb0276646a/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c9ec605ace243b6dbe3bd27ebdd5d33b00d8d1d3f580b39fdd15cd96fd71796", size = 277766, upload-time = "2025-10-19T22:37:23.779Z" }, + { url = "https://files.pythonhosted.org/packages/16/6e/c0fb547eef61293153348f12e0f75a06abb322664b34a1573a7760501336/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:808527f2407f58a76c916d6aa15d58692a4a019fdf8d4c32ac7ff303b7d7af09", size = 278105, upload-time = "2025-10-19T22:26:56.821Z" }, + { url = "https://files.pythonhosted.org/packages/2d/b1/b9c75e03b768f61cf2e84ee193dc18601aeaf89a4684b20f2f0e9f52b62c/fastuuid-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fb3c0d7fef6674bbeacdd6dbd386924a7b60b26de849266d1ff6602937675c8", size = 301564, upload-time = "2025-10-19T22:30:31.604Z" }, + { url = "https://files.pythonhosted.org/packages/fc/fa/f7395fdac07c7a54f18f801744573707321ca0cee082e638e36452355a9d/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab3f5d36e4393e628a4df337c2c039069344db5f4b9d2a3c9cea48284f1dd741", size = 459659, upload-time = "2025-10-19T22:31:32.341Z" }, + { url = "https://files.pythonhosted.org/packages/66/49/c9fd06a4a0b1f0f048aacb6599e7d96e5d6bc6fa680ed0d46bf111929d1b/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b9a0ca4f03b7e0b01425281ffd44e99d360e15c895f1907ca105854ed85e2057", size = 478430, upload-time = "2025-10-19T22:26:22.962Z" }, + { url = "https://files.pythonhosted.org/packages/be/9c/909e8c95b494e8e140e8be6165d5fc3f61fdc46198c1554df7b3e1764471/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3acdf655684cc09e60fb7e4cf524e8f42ea760031945aa8086c7eae2eeeabeb8", size = 450894, upload-time = "2025-10-19T22:27:01.647Z" }, + { url = "https://files.pythonhosted.org/packages/90/eb/d29d17521976e673c55ef7f210d4cdd72091a9ec6755d0fd4710d9b3c871/fastuuid-0.14.0-cp312-cp312-win32.whl", hash = "sha256:9579618be6280700ae36ac42c3efd157049fe4dd40ca49b021280481c78c3176", size = 154374, upload-time = "2025-10-19T22:29:19.879Z" }, + { url = "https://files.pythonhosted.org/packages/cc/fc/f5c799a6ea6d877faec0472d0b27c079b47c86b1cdc577720a5386483b36/fastuuid-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:d9e4332dc4ba054434a9594cbfaf7823b57993d7d8e7267831c3e059857cf397", size = 156550, upload-time = "2025-10-19T22:27:49.658Z" }, +] + +[[package]] +name = "ffmpy" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/d2/1c4c582d71bcc65c76fa69fab85de6257d50fdf6fd4a2317c53917e9a581/ffmpy-1.0.0.tar.gz", hash = "sha256:b12932e95435c8820f1cd041024402765f821971e4bae753b327fc02a6e12f8b", size = 5101, upload-time = "2025-11-11T06:24:23.856Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/56/dd3669eccebb6d8ac81e624542ebd53fe6f08e1b8f2f8d50aeb7e3b83f99/ffmpy-1.0.0-py3-none-any.whl", hash = "sha256:5640e5f0fd03fb6236d0e119b16ccf6522db1c826fdf35dcb87087b60fd7504f", size = 5614, upload-time = "2025-11-11T06:24:22.818Z" }, +] + +[[package]] +name = "filelock" +version = "3.20.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, +] + +[[package]] +name = "flask" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" }, +] + +[[package]] +name = "flatbuffers" +version = "25.12.19" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" }, +] + +[[package]] +name = "fonttools" +version = "4.61.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/ca/cf17b88a8df95691275a3d77dc0a5ad9907f328ae53acbe6795da1b2f5ed/fonttools-4.61.1.tar.gz", hash = "sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69", size = 3565756, upload-time = "2025-12-12T17:31:24.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/12/bf9f4eaa2fad039356cc627587e30ed008c03f1cebd3034376b5ee8d1d44/fonttools-4.61.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c6604b735bb12fef8e0efd5578c9fb5d3d8532d5001ea13a19cddf295673ee09", size = 2852213, upload-time = "2025-12-12T17:29:46.675Z" }, + { url = "https://files.pythonhosted.org/packages/ac/49/4138d1acb6261499bedde1c07f8c2605d1d8f9d77a151e5507fd3ef084b6/fonttools-4.61.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5ce02f38a754f207f2f06557523cd39a06438ba3aafc0639c477ac409fc64e37", size = 2401689, upload-time = "2025-12-12T17:29:48.769Z" }, + { url = "https://files.pythonhosted.org/packages/e5/fe/e6ce0fe20a40e03aef906af60aa87668696f9e4802fa283627d0b5ed777f/fonttools-4.61.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77efb033d8d7ff233385f30c62c7c79271c8885d5c9657d967ede124671bbdfb", size = 5058809, upload-time = "2025-12-12T17:29:51.701Z" }, + { url = "https://files.pythonhosted.org/packages/79/61/1ca198af22f7dd22c17ab86e9024ed3c06299cfdb08170640e9996d501a0/fonttools-4.61.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:75c1a6dfac6abd407634420c93864a1e274ebc1c7531346d9254c0d8f6ca00f9", size = 5036039, upload-time = "2025-12-12T17:29:53.659Z" }, + { url = "https://files.pythonhosted.org/packages/99/cc/fa1801e408586b5fce4da9f5455af8d770f4fc57391cd5da7256bb364d38/fonttools-4.61.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0de30bfe7745c0d1ffa2b0b7048fb7123ad0d71107e10ee090fa0b16b9452e87", size = 5034714, upload-time = "2025-12-12T17:29:55.592Z" }, + { url = "https://files.pythonhosted.org/packages/bf/aa/b7aeafe65adb1b0a925f8f25725e09f078c635bc22754f3fecb7456955b0/fonttools-4.61.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:58b0ee0ab5b1fc9921eccfe11d1435added19d6494dde14e323f25ad2bc30c56", size = 5158648, upload-time = "2025-12-12T17:29:57.861Z" }, + { url = "https://files.pythonhosted.org/packages/99/f9/08ea7a38663328881384c6e7777bbefc46fd7d282adfd87a7d2b84ec9d50/fonttools-4.61.1-cp311-cp311-win32.whl", hash = "sha256:f79b168428351d11e10c5aeb61a74e1851ec221081299f4cf56036a95431c43a", size = 2280681, upload-time = "2025-12-12T17:29:59.943Z" }, + { url = "https://files.pythonhosted.org/packages/07/ad/37dd1ae5fa6e01612a1fbb954f0927681f282925a86e86198ccd7b15d515/fonttools-4.61.1-cp311-cp311-win_amd64.whl", hash = "sha256:fe2efccb324948a11dd09d22136fe2ac8a97d6c1347cf0b58a911dcd529f66b7", size = 2331951, upload-time = "2025-12-12T17:30:02.254Z" }, + { url = "https://files.pythonhosted.org/packages/6f/16/7decaa24a1bd3a70c607b2e29f0adc6159f36a7e40eaba59846414765fd4/fonttools-4.61.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f3cb4a569029b9f291f88aafc927dd53683757e640081ca8c412781ea144565e", size = 2851593, upload-time = "2025-12-12T17:30:04.225Z" }, + { url = "https://files.pythonhosted.org/packages/94/98/3c4cb97c64713a8cf499b3245c3bf9a2b8fd16a3e375feff2aed78f96259/fonttools-4.61.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41a7170d042e8c0024703ed13b71893519a1a6d6e18e933e3ec7507a2c26a4b2", size = 2400231, upload-time = "2025-12-12T17:30:06.47Z" }, + { url = "https://files.pythonhosted.org/packages/b7/37/82dbef0f6342eb01f54bca073ac1498433d6ce71e50c3c3282b655733b31/fonttools-4.61.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10d88e55330e092940584774ee5e8a6971b01fc2f4d3466a1d6c158230880796", size = 4954103, upload-time = "2025-12-12T17:30:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/6c/44/f3aeac0fa98e7ad527f479e161aca6c3a1e47bb6996b053d45226fe37bf2/fonttools-4.61.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15acc09befd16a0fb8a8f62bc147e1a82817542d72184acca9ce6e0aeda9fa6d", size = 5004295, upload-time = "2025-12-12T17:30:10.56Z" }, + { url = "https://files.pythonhosted.org/packages/14/e8/7424ced75473983b964d09f6747fa09f054a6d656f60e9ac9324cf40c743/fonttools-4.61.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e6bcdf33aec38d16508ce61fd81838f24c83c90a1d1b8c68982857038673d6b8", size = 4944109, upload-time = "2025-12-12T17:30:12.874Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8b/6391b257fa3d0b553d73e778f953a2f0154292a7a7a085e2374b111e5410/fonttools-4.61.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5fade934607a523614726119164ff621e8c30e8fa1ffffbbd358662056ba69f0", size = 5093598, upload-time = "2025-12-12T17:30:15.79Z" }, + { url = "https://files.pythonhosted.org/packages/d9/71/fd2ea96cdc512d92da5678a1c98c267ddd4d8c5130b76d0f7a80f9a9fde8/fonttools-4.61.1-cp312-cp312-win32.whl", hash = "sha256:75da8f28eff26defba42c52986de97b22106cb8f26515b7c22443ebc9c2d3261", size = 2269060, upload-time = "2025-12-12T17:30:18.058Z" }, + { url = "https://files.pythonhosted.org/packages/80/3b/a3e81b71aed5a688e89dfe0e2694b26b78c7d7f39a5ffd8a7d75f54a12a8/fonttools-4.61.1-cp312-cp312-win_amd64.whl", hash = "sha256:497c31ce314219888c0e2fce5ad9178ca83fe5230b01a5006726cdf3ac9f24d9", size = 2319078, upload-time = "2025-12-12T17:30:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371", size = 1148996, upload-time = "2025-12-12T17:31:21.03Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, + { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, + { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, + { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, + { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, + { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, + { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/7d/5df2650c57d47c57232af5ef4b4fdbff182070421e405e0d62c6cdbfaa87/fsspec-2026.1.0.tar.gz", hash = "sha256:e987cb0496a0d81bba3a9d1cee62922fb395e7d4c3b575e57f547953334fe07b", size = 310496, upload-time = "2026-01-09T15:21:35.562Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/c9/97cc5aae1648dcb851958a3ddf73ccd7dbe5650d95203ecb4d7720b4cdbf/fsspec-2026.1.0-py3-none-any.whl", hash = "sha256:cb76aa913c2285a3b49bdd5fc55b1d7c708d7208126b60f2eb8194fe1b4cbdcc", size = 201838, upload-time = "2026-01-09T15:21:34.041Z" }, +] + +[package.optional-dependencies] +http = [ + { name = "aiohttp" }, +] + +[[package]] +name = "gradio" +version = "6.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiofiles" }, + { name = "anyio" }, + { name = "brotli" }, + { name = "fastapi" }, + { name = "ffmpy" }, + { name = "gradio-client" }, + { name = "groovy" }, + { name = "httpx" }, + { name = "huggingface-hub" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "numpy" }, + { name = "orjson" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "pillow" }, + { name = "pydantic" }, + { name = "pydub" }, + { name = "python-multipart" }, + { name = "pytz" }, + { name = "pyyaml" }, + { name = "safehttpx" }, + { name = "semantic-version" }, + { name = "starlette" }, + { name = "tomlkit" }, + { name = "typer" }, + { name = "typing-extensions" }, + { name = "uvicorn" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/4f/b095b9a9ddc1ba433121f390df0e8a20a3360ffabd43ec13e86d6ce412b4/gradio-6.5.1.tar.gz", hash = "sha256:31223a1699f15072176dbf48a94f08457228a38263bb4c221a0ccea3a639a595", size = 40132899, upload-time = "2026-01-29T16:39:14.195Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/72/4f56a920147ce215e2286defc257a613b3b6d8c90cea323758a99ca0f9fa/gradio-6.5.1-py3-none-any.whl", hash = "sha256:5d49ff9691413ca5411189a694de5cbf1b171e2d49bf9f113952ae8a93c7088d", size = 24183125, upload-time = "2026-01-29T16:39:10.908Z" }, +] + +[[package]] +name = "gradio-client" +version = "2.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fsspec" }, + { name = "httpx" }, + { name = "huggingface-hub" }, + { name = "packaging" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d4/75/5c971cc80a6a477f038c66869178684c5010fd61b232277c120c61588d74/gradio_client-2.0.3.tar.gz", hash = "sha256:8f1cec02dccaf64ac0285ed60479a2b0db3778dfe74c85a36d7ec9a95daeccc4", size = 55027, upload-time = "2026-01-10T00:03:56.446Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/11/758b76a14e1783549c71828b36e81c997b99683bc4ec14b28417dff3348f/gradio_client-2.0.3-py3-none-any.whl", hash = "sha256:bcc88da74e3a387bcd41535578abbafe2091bcf4715c9542111804741b9e50b0", size = 55669, upload-time = "2026-01-10T00:03:54.262Z" }, +] + +[[package]] +name = "greenlet" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/e5/40dbda2736893e3e53d25838e0f19a2b417dfc122b9989c91918db30b5d3/greenlet-3.3.0.tar.gz", hash = "sha256:a82bb225a4e9e4d653dd2fb7b8b2d36e4fb25bc0165422a11e48b88e9e6f78fb", size = 190651, upload-time = "2025-12-04T14:49:44.05Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/cb/48e964c452ca2b92175a9b2dca037a553036cb053ba69e284650ce755f13/greenlet-3.3.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e29f3018580e8412d6aaf5641bb7745d38c85228dacf51a73bd4e26ddf2a6a8e", size = 274908, upload-time = "2025-12-04T14:23:26.435Z" }, + { url = "https://files.pythonhosted.org/packages/28/da/38d7bff4d0277b594ec557f479d65272a893f1f2a716cad91efeb8680953/greenlet-3.3.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a687205fb22794e838f947e2194c0566d3812966b41c78709554aa883183fb62", size = 577113, upload-time = "2025-12-04T14:50:05.493Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f2/89c5eb0faddc3ff014f1c04467d67dee0d1d334ab81fadbf3744847f8a8a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4243050a88ba61842186cb9e63c7dfa677ec146160b0efd73b855a3d9c7fcf32", size = 590338, upload-time = "2025-12-04T14:57:41.136Z" }, + { url = "https://files.pythonhosted.org/packages/80/d7/db0a5085035d05134f8c089643da2b44cc9b80647c39e93129c5ef170d8f/greenlet-3.3.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:670d0f94cd302d81796e37299bcd04b95d62403883b24225c6b5271466612f45", size = 601098, upload-time = "2025-12-04T15:07:11.898Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a6/e959a127b630a58e23529972dbc868c107f9d583b5a9f878fb858c46bc1a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb3a8ec3db4a3b0eb8a3c25436c2d49e3505821802074969db017b87bc6a948", size = 590206, upload-time = "2025-12-04T14:26:01.254Z" }, + { url = "https://files.pythonhosted.org/packages/48/60/29035719feb91798693023608447283b266b12efc576ed013dd9442364bb/greenlet-3.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2de5a0b09eab81fc6a382791b995b1ccf2b172a9fec934747a7a23d2ff291794", size = 1550668, upload-time = "2025-12-04T15:04:22.439Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5f/783a23754b691bfa86bd72c3033aa107490deac9b2ef190837b860996c9f/greenlet-3.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4449a736606bd30f27f8e1ff4678ee193bc47f6ca810d705981cfffd6ce0d8c5", size = 1615483, upload-time = "2025-12-04T14:27:28.083Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d5/c339b3b4bc8198b7caa4f2bd9fd685ac9f29795816d8db112da3d04175bb/greenlet-3.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:7652ee180d16d447a683c04e4c5f6441bae7ba7b17ffd9f6b3aff4605e9e6f71", size = 301164, upload-time = "2025-12-04T14:42:51.577Z" }, + { url = "https://files.pythonhosted.org/packages/f8/0a/a3871375c7b9727edaeeea994bfff7c63ff7804c9829c19309ba2e058807/greenlet-3.3.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:b01548f6e0b9e9784a2c99c5651e5dc89ffcbe870bc5fb2e5ef864e9cc6b5dcb", size = 276379, upload-time = "2025-12-04T14:23:30.498Z" }, + { url = "https://files.pythonhosted.org/packages/43/ab/7ebfe34dce8b87be0d11dae91acbf76f7b8246bf9d6b319c741f99fa59c6/greenlet-3.3.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349345b770dc88f81506c6861d22a6ccd422207829d2c854ae2af8025af303e3", size = 597294, upload-time = "2025-12-04T14:50:06.847Z" }, + { url = "https://files.pythonhosted.org/packages/a4/39/f1c8da50024feecd0793dbd5e08f526809b8ab5609224a2da40aad3a7641/greenlet-3.3.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e8e18ed6995e9e2c0b4ed264d2cf89260ab3ac7e13555b8032b25a74c6d18655", size = 607742, upload-time = "2025-12-04T14:57:42.349Z" }, + { url = "https://files.pythonhosted.org/packages/77/cb/43692bcd5f7a0da6ec0ec6d58ee7cddb606d055ce94a62ac9b1aa481e969/greenlet-3.3.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c024b1e5696626890038e34f76140ed1daf858e37496d33f2af57f06189e70d7", size = 622297, upload-time = "2025-12-04T15:07:13.552Z" }, + { url = "https://files.pythonhosted.org/packages/75/b0/6bde0b1011a60782108c01de5913c588cf51a839174538d266de15e4bf4d/greenlet-3.3.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:047ab3df20ede6a57c35c14bf5200fcf04039d50f908270d3f9a7a82064f543b", size = 609885, upload-time = "2025-12-04T14:26:02.368Z" }, + { url = "https://files.pythonhosted.org/packages/49/0e/49b46ac39f931f59f987b7cd9f34bfec8ef81d2a1e6e00682f55be5de9f4/greenlet-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d9ad37fc657b1102ec880e637cccf20191581f75c64087a549e66c57e1ceb53", size = 1567424, upload-time = "2025-12-04T15:04:23.757Z" }, + { url = "https://files.pythonhosted.org/packages/05/f5/49a9ac2dff7f10091935def9165c90236d8f175afb27cbed38fb1d61ab6b/greenlet-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83cd0e36932e0e7f36a64b732a6f60c2fc2df28c351bae79fbaf4f8092fe7614", size = 1636017, upload-time = "2025-12-04T14:27:29.688Z" }, + { url = "https://files.pythonhosted.org/packages/6c/79/3912a94cf27ec503e51ba493692d6db1e3cd8ac7ac52b0b47c8e33d7f4f9/greenlet-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7a34b13d43a6b78abf828a6d0e87d3385680eaf830cd60d20d52f249faabf39", size = 301964, upload-time = "2025-12-04T14:36:58.316Z" }, +] + +[[package]] +name = "groovy" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/36/bbdede67400277bef33d3ec0e6a31750da972c469f75966b4930c753218f/groovy-0.1.2.tar.gz", hash = "sha256:25c1dc09b3f9d7e292458aa762c6beb96ea037071bf5e917fc81fb78d2231083", size = 17325, upload-time = "2025-02-28T20:24:56.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/27/3d6dcadc8a3214d8522c1e7f6a19554e33659be44546d44a2f7572ac7d2a/groovy-0.1.2-py3-none-any.whl", hash = "sha256:7f7975bab18c729a257a8b1ae9dcd70b7cafb1720481beae47719af57c35fa64", size = 14090, upload-time = "2025-02-28T20:24:55.152Z" }, +] + +[[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 = "hf-xet" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/6e/0f11bacf08a67f7fb5ee09740f2ca54163863b07b70d579356e9222ce5d8/hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f", size = 506020, upload-time = "2025-10-24T19:04:32.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/2d/22338486473df5923a9ab7107d375dbef9173c338ebef5098ef593d2b560/hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848", size = 2866099, upload-time = "2025-10-24T19:04:15.366Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8c/c5becfa53234299bc2210ba314eaaae36c2875e0045809b82e40a9544f0c/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4", size = 2722178, upload-time = "2025-10-24T19:04:13.695Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/cf3ab0b652b082e66876d08da57fcc6fa2f0e6c70dfbbafbd470bb73eb47/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd", size = 3320214, upload-time = "2025-10-24T19:04:03.596Z" }, + { url = "https://files.pythonhosted.org/packages/46/92/3f7ec4a1b6a65bf45b059b6d4a5d38988f63e193056de2f420137e3c3244/hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c", size = 3229054, upload-time = "2025-10-24T19:04:01.949Z" }, + { url = "https://files.pythonhosted.org/packages/0b/dd/7ac658d54b9fb7999a0ccb07ad863b413cbaf5cf172f48ebcd9497ec7263/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737", size = 3413812, upload-time = "2025-10-24T19:04:24.585Z" }, + { url = "https://files.pythonhosted.org/packages/92/68/89ac4e5b12a9ff6286a12174c8538a5930e2ed662091dd2572bbe0a18c8a/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865", size = 3508920, upload-time = "2025-10-24T19:04:26.927Z" }, + { url = "https://files.pythonhosted.org/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735, upload-time = "2025-10-24T19:04:35.928Z" }, +] + +[[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.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/08/17e07e8d89ab8f343c134616d72eebfe03798835058e2ab579dcc8353c06/httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657", size = 206521, upload-time = "2025-10-10T03:54:31.002Z" }, + { url = "https://files.pythonhosted.org/packages/aa/06/c9c1b41ff52f16aee526fd10fbda99fa4787938aa776858ddc4a1ea825ec/httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70", size = 110375, upload-time = "2025-10-10T03:54:31.941Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cc/10935db22fda0ee34c76f047590ca0a8bd9de531406a3ccb10a90e12ea21/httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df", size = 456621, upload-time = "2025-10-10T03:54:33.176Z" }, + { url = "https://files.pythonhosted.org/packages/0e/84/875382b10d271b0c11aa5d414b44f92f8dd53e9b658aec338a79164fa548/httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e", size = 454954, upload-time = "2025-10-10T03:54:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/30/e1/44f89b280f7e46c0b1b2ccee5737d46b3bb13136383958f20b580a821ca0/httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274", size = 440175, upload-time = "2025-10-10T03:54:35.942Z" }, + { url = "https://files.pythonhosted.org/packages/6f/7e/b9287763159e700e335028bc1824359dc736fa9b829dacedace91a39b37e/httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec", size = 440310, upload-time = "2025-10-10T03:54:37.1Z" }, + { url = "https://files.pythonhosted.org/packages/b3/07/5b614f592868e07f5c94b1f301b5e14a21df4e8076215a3bccb830a687d8/httptools-0.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb", size = 86875, upload-time = "2025-10-10T03:54:38.421Z" }, + { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, + { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, + { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, + { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, + { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" }, +] + +[[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.optional-dependencies] +socks = [ + { name = "socksio" }, +] + +[[package]] +name = "huggingface-hub" +version = "0.36.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/63/4910c5fa9128fdadf6a9c5ac138e8b1b6cee4ca44bf7915bbfbce4e355ee/huggingface_hub-0.36.0.tar.gz", hash = "sha256:47b3f0e2539c39bf5cde015d63b72ec49baff67b6931c3d97f3f84532e2b8d25", size = 463358, upload-time = "2025-10-23T12:12:01.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/bd/1a875e0d592d447cbc02805fd3fe0f497714d6a2583f59d14fa9ebad96eb/huggingface_hub-0.36.0-py3-none-any.whl", hash = "sha256:7bcc9ad17d5b3f07b57c78e79d527102d08313caa278a641993acddcb894548d", size = 566094, upload-time = "2025-10-23T12:11:59.557Z" }, +] + +[[package]] +name = "humanfriendly" +version = "10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyreadline3", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702, upload-time = "2021-09-17T21:40:43.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794, upload-time = "2021-09-17T21:40:39.897Z" }, +] + +[[package]] +name = "hyperpyyaml" +version = "1.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, + { name = "ruamel-yaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/cf/000268ec653f354d464e6101505672365e8cecdcb216034a4bb0dbf9dca0/hyperpyyaml-1.2.3.tar.gz", hash = "sha256:4b135800ae13b846ae90aeb1c25e65e4d7a0138bd4935d3222734828d9f218f6", size = 17355, upload-time = "2026-01-01T20:01:49.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/a5/98e20cc4365e293dfc1382e2f8dbf44b1cb14d75658ab5147cb0d6d8718e/hyperpyyaml-1.2.3-py3-none-any.whl", hash = "sha256:0088c8ce97dc7c7d3460112b6ccc92dca46fade4d0320bc39f58b1fdda60f7f1", size = 16456, upload-time = "2026-01-01T20:01:48.462Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304, upload-time = "2024-09-11T14:56:08.937Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514, upload-time = "2024-09-11T14:56:07.019Z" }, +] + +[[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 = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + +[[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 = "jiter" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294, upload-time = "2025-11-09T20:49:23.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/f9/eaca4633486b527ebe7e681c431f529b63fe2709e7c5242fc0f43f77ce63/jiter-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8f8a7e317190b2c2d60eb2e8aa835270b008139562d70fe732e1c0020ec53c9", size = 316435, upload-time = "2025-11-09T20:47:02.087Z" }, + { url = "https://files.pythonhosted.org/packages/10/c1/40c9f7c22f5e6ff715f28113ebaba27ab85f9af2660ad6e1dd6425d14c19/jiter-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2218228a077e784c6c8f1a8e5d6b8cb1dea62ce25811c356364848554b2056cd", size = 320548, upload-time = "2025-11-09T20:47:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1b/efbb68fe87e7711b00d2cfd1f26bb4bfc25a10539aefeaa7727329ffb9cb/jiter-0.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9354ccaa2982bf2188fd5f57f79f800ef622ec67beb8329903abf6b10da7d423", size = 351915, upload-time = "2025-11-09T20:47:05.171Z" }, + { url = "https://files.pythonhosted.org/packages/15/2d/c06e659888c128ad1e838123d0638f0efad90cc30860cb5f74dd3f2fc0b3/jiter-0.12.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2607185ea89b4af9a604d4c7ec40e45d3ad03ee66998b031134bc510232bb7", size = 368966, upload-time = "2025-11-09T20:47:06.508Z" }, + { url = "https://files.pythonhosted.org/packages/6b/20/058db4ae5fb07cf6a4ab2e9b9294416f606d8e467fb74c2184b2a1eeacba/jiter-0.12.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a585a5e42d25f2e71db5f10b171f5e5ea641d3aa44f7df745aa965606111cc2", size = 482047, upload-time = "2025-11-09T20:47:08.382Z" }, + { url = "https://files.pythonhosted.org/packages/49/bb/dc2b1c122275e1de2eb12905015d61e8316b2f888bdaac34221c301495d6/jiter-0.12.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd9e21d34edff5a663c631f850edcb786719c960ce887a5661e9c828a53a95d9", size = 380835, upload-time = "2025-11-09T20:47:09.81Z" }, + { url = "https://files.pythonhosted.org/packages/23/7d/38f9cd337575349de16da575ee57ddb2d5a64d425c9367f5ef9e4612e32e/jiter-0.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a612534770470686cd5431478dc5a1b660eceb410abade6b1b74e320ca98de6", size = 364587, upload-time = "2025-11-09T20:47:11.529Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a3/b13e8e61e70f0bb06085099c4e2462647f53cc2ca97614f7fedcaa2bb9f3/jiter-0.12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3985aea37d40a908f887b34d05111e0aae822943796ebf8338877fee2ab67725", size = 390492, upload-time = "2025-11-09T20:47:12.993Z" }, + { url = "https://files.pythonhosted.org/packages/07/71/e0d11422ed027e21422f7bc1883c61deba2d9752b720538430c1deadfbca/jiter-0.12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b1207af186495f48f72529f8d86671903c8c10127cac6381b11dddc4aaa52df6", size = 522046, upload-time = "2025-11-09T20:47:14.6Z" }, + { url = "https://files.pythonhosted.org/packages/9f/59/b968a9aa7102a8375dbbdfbd2aeebe563c7e5dddf0f47c9ef1588a97e224/jiter-0.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef2fb241de583934c9915a33120ecc06d94aa3381a134570f59eed784e87001e", size = 513392, upload-time = "2025-11-09T20:47:16.011Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e4/7df62002499080dbd61b505c5cb351aa09e9959d176cac2aa8da6f93b13b/jiter-0.12.0-cp311-cp311-win32.whl", hash = "sha256:453b6035672fecce8007465896a25b28a6b59cfe8fbc974b2563a92f5a92a67c", size = 206096, upload-time = "2025-11-09T20:47:17.344Z" }, + { url = "https://files.pythonhosted.org/packages/bb/60/1032b30ae0572196b0de0e87dce3b6c26a1eff71aad5fe43dee3082d32e0/jiter-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:ca264b9603973c2ad9435c71a8ec8b49f8f715ab5ba421c85a51cde9887e421f", size = 204899, upload-time = "2025-11-09T20:47:19.365Z" }, + { url = "https://files.pythonhosted.org/packages/49/d5/c145e526fccdb834063fb45c071df78b0cc426bbaf6de38b0781f45d956f/jiter-0.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:cb00ef392e7d684f2754598c02c409f376ddcef857aae796d559e6cacc2d78a5", size = 188070, upload-time = "2025-11-09T20:47:20.75Z" }, + { url = "https://files.pythonhosted.org/packages/92/c9/5b9f7b4983f1b542c64e84165075335e8a236fa9e2ea03a0c79780062be8/jiter-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:305e061fa82f4680607a775b2e8e0bcb071cd2205ac38e6ef48c8dd5ebe1cf37", size = 314449, upload-time = "2025-11-09T20:47:22.999Z" }, + { url = "https://files.pythonhosted.org/packages/98/6e/e8efa0e78de00db0aee82c0cf9e8b3f2027efd7f8a71f859d8f4be8e98ef/jiter-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c1860627048e302a528333c9307c818c547f214d8659b0705d2195e1a94b274", size = 319855, upload-time = "2025-11-09T20:47:24.779Z" }, + { url = "https://files.pythonhosted.org/packages/20/26/894cd88e60b5d58af53bec5c6759d1292bd0b37a8b5f60f07abf7a63ae5f/jiter-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df37577a4f8408f7e0ec3205d2a8f87672af8f17008358063a4d6425b6081ce3", size = 350171, upload-time = "2025-11-09T20:47:26.469Z" }, + { url = "https://files.pythonhosted.org/packages/f5/27/a7b818b9979ac31b3763d25f3653ec3a954044d5e9f5d87f2f247d679fd1/jiter-0.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fdd787356c1c13a4f40b43c2156276ef7a71eb487d98472476476d803fb2cf", size = 365590, upload-time = "2025-11-09T20:47:27.918Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7e/e46195801a97673a83746170b17984aa8ac4a455746354516d02ca5541b4/jiter-0.12.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1eb5db8d9c65b112aacf14fcd0faae9913d07a8afea5ed06ccdd12b724e966a1", size = 479462, upload-time = "2025-11-09T20:47:29.654Z" }, + { url = "https://files.pythonhosted.org/packages/ca/75/f833bfb009ab4bd11b1c9406d333e3b4357709ed0570bb48c7c06d78c7dd/jiter-0.12.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73c568cc27c473f82480abc15d1301adf333a7ea4f2e813d6a2c7d8b6ba8d0df", size = 378983, upload-time = "2025-11-09T20:47:31.026Z" }, + { url = "https://files.pythonhosted.org/packages/71/b3/7a69d77943cc837d30165643db753471aff5df39692d598da880a6e51c24/jiter-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4321e8a3d868919bcb1abb1db550d41f2b5b326f72df29e53b2df8b006eb9403", size = 361328, upload-time = "2025-11-09T20:47:33.286Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ac/a78f90caf48d65ba70d8c6efc6f23150bc39dc3389d65bbec2a95c7bc628/jiter-0.12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a51bad79f8cc9cac2b4b705039f814049142e0050f30d91695a2d9a6611f126", size = 386740, upload-time = "2025-11-09T20:47:34.703Z" }, + { url = "https://files.pythonhosted.org/packages/39/b6/5d31c2cc8e1b6a6bcf3c5721e4ca0a3633d1ab4754b09bc7084f6c4f5327/jiter-0.12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a67b678f6a5f1dd6c36d642d7db83e456bc8b104788262aaefc11a22339f5a9", size = 520875, upload-time = "2025-11-09T20:47:36.058Z" }, + { url = "https://files.pythonhosted.org/packages/30/b5/4df540fae4e9f68c54b8dab004bd8c943a752f0b00efd6e7d64aa3850339/jiter-0.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efe1a211fe1fd14762adea941e3cfd6c611a136e28da6c39272dbb7a1bbe6a86", size = 511457, upload-time = "2025-11-09T20:47:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/07/65/86b74010e450a1a77b2c1aabb91d4a91dd3cd5afce99f34d75fd1ac64b19/jiter-0.12.0-cp312-cp312-win32.whl", hash = "sha256:d779d97c834b4278276ec703dc3fc1735fca50af63eb7262f05bdb4e62203d44", size = 204546, upload-time = "2025-11-09T20:47:40.47Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c7/6659f537f9562d963488e3e55573498a442503ced01f7e169e96a6110383/jiter-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e8269062060212b373316fe69236096aaf4c49022d267c6736eebd66bbbc60bb", size = 205196, upload-time = "2025-11-09T20:47:41.794Z" }, + { url = "https://files.pythonhosted.org/packages/21/f4/935304f5169edadfec7f9c01eacbce4c90bb9a82035ac1de1f3bd2d40be6/jiter-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:06cb970936c65de926d648af0ed3d21857f026b1cf5525cb2947aa5e01e05789", size = 186100, upload-time = "2025-11-09T20:47:43.007Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/5339ef1ecaa881c6948669956567a64d2670941925f245c434f494ffb0e5/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:4739a4657179ebf08f85914ce50332495811004cc1747852e8b2041ed2aab9b8", size = 311144, upload-time = "2025-11-09T20:49:10.503Z" }, + { url = "https://files.pythonhosted.org/packages/27/74/3446c652bffbd5e81ab354e388b1b5fc1d20daac34ee0ed11ff096b1b01a/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:41da8def934bf7bec16cb24bd33c0ca62126d2d45d81d17b864bd5ad721393c3", size = 305877, upload-time = "2025-11-09T20:49:12.269Z" }, + { url = "https://files.pythonhosted.org/packages/a1/f4/ed76ef9043450f57aac2d4fbeb27175aa0eb9c38f833be6ef6379b3b9a86/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c44ee814f499c082e69872d426b624987dbc5943ab06e9bbaa4f81989fdb79e", size = 340419, upload-time = "2025-11-09T20:49:13.803Z" }, + { url = "https://files.pythonhosted.org/packages/21/01/857d4608f5edb0664aa791a3d45702e1a5bcfff9934da74035e7b9803846/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd2097de91cf03eaa27b3cbdb969addf83f0179c6afc41bbc4513705e013c65d", size = 347212, upload-time = "2025-11-09T20:49:15.643Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f5/12efb8ada5f5c9edc1d4555fe383c1fb2eac05ac5859258a72d61981d999/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:e8547883d7b96ef2e5fe22b88f8a4c8725a56e7f4abafff20fd5272d634c7ecb", size = 309974, upload-time = "2025-11-09T20:49:17.187Z" }, + { url = "https://files.pythonhosted.org/packages/85/15/d6eb3b770f6a0d332675141ab3962fd4a7c270ede3515d9f3583e1d28276/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:89163163c0934854a668ed783a2546a0617f71706a2551a4a0666d91ab365d6b", size = 304233, upload-time = "2025-11-09T20:49:18.734Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3e/e7e06743294eea2cf02ced6aa0ff2ad237367394e37a0e2b4a1108c67a36/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d96b264ab7d34bbb2312dedc47ce07cd53f06835eacbc16dde3761f47c3a9e7f", size = 338537, upload-time = "2025-11-09T20:49:20.317Z" }, + { url = "https://files.pythonhosted.org/packages/2f/9c/6753e6522b8d0ef07d3a3d239426669e984fb0eba15a315cdbc1253904e4/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110, upload-time = "2025-11-09T20:49:21.817Z" }, +] + +[[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 = "jsonschema" +version = "4.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/2e/03362ee4034a4c917f697890ccd4aec0800ccf9ded7f511971c75451deec/jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", size = 325778, upload-time = "2024-07-08T18:40:05.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/4a/4f9dbeb84e8850557c02365a0eee0649abe5eb1d84af92a25731c6c0f922/jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566", size = 88462, upload-time = "2024-07-08T18:40:00.165Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "julius" +version = "0.2.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "torch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/19/c9e1596b5572c786b93428d0904280e964c930fae7e6c9368ed9e1b63922/julius-0.2.7.tar.gz", hash = "sha256:3c0f5f5306d7d6016fcc95196b274cae6f07e2c9596eed314e4e7641554fbb08", size = 59640, upload-time = "2022-09-19T16:13:34.2Z" } + +[[package]] +name = "kiwisolver" +version = "1.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167, upload-time = "2025-08-10T21:25:53.403Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579, upload-time = "2025-08-10T21:25:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309, upload-time = "2025-08-10T21:25:55.76Z" }, + { url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596, upload-time = "2025-08-10T21:25:56.861Z" }, + { url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548, upload-time = "2025-08-10T21:25:58.246Z" }, + { url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618, upload-time = "2025-08-10T21:25:59.857Z" }, + { url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437, upload-time = "2025-08-10T21:26:01.105Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742, upload-time = "2025-08-10T21:26:02.675Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810, upload-time = "2025-08-10T21:26:04.009Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579, upload-time = "2025-08-10T21:26:05.317Z" }, + { url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071, upload-time = "2025-08-10T21:26:06.686Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840, upload-time = "2025-08-10T21:26:07.94Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159, upload-time = "2025-08-10T21:26:09.048Z" }, + { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" }, + { url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756, upload-time = "2025-08-10T21:26:13.096Z" }, + { url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404, upload-time = "2025-08-10T21:26:14.457Z" }, + { url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410, upload-time = "2025-08-10T21:26:15.73Z" }, + { url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631, upload-time = "2025-08-10T21:26:17.045Z" }, + { url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963, upload-time = "2025-08-10T21:26:18.737Z" }, + { url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" }, + { url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" }, + { url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" }, + { url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" }, + { url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" }, + { url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104, upload-time = "2025-08-10T21:27:43.287Z" }, + { url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592, upload-time = "2025-08-10T21:27:44.314Z" }, + { url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281, upload-time = "2025-08-10T21:27:45.369Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009, upload-time = "2025-08-10T21:27:46.376Z" }, + { url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" }, +] + +[[package]] +name = "lazy-loader" +version = "0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/6b/c875b30a1ba490860c93da4cabf479e03f584eba06fe5963f6f6644653d8/lazy_loader-0.4.tar.gz", hash = "sha256:47c75182589b91a4e1a85a136c074285a5ad4d9f39c63e0d7fb76391c4574cd1", size = 15431, upload-time = "2024-04-05T13:03:12.261Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/60/d497a310bde3f01cb805196ac61b7ad6dc5dcf8dce66634dc34364b20b4f/lazy_loader-0.4-py3-none-any.whl", hash = "sha256:342aa8e14d543a154047afb4ba8ef17f5563baad3fc610d7b15b213b0f119efc", size = 12097, upload-time = "2024-04-05T13:03:10.514Z" }, +] + +[[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 = "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 = "lightning" +version = "2.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fsspec", extra = ["http"] }, + { name = "lightning-utilities" }, + { name = "packaging" }, + { name = "pytorch-lightning" }, + { name = "pyyaml" }, + { name = "torch" }, + { name = "torchmetrics" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/d5/892ea38816925b3511493e87b0b32494122bf8a20e66f4f2cd2667f95625/lightning-2.6.0.tar.gz", hash = "sha256:881841716b59c1837ae0c562c2e64fea9bcf49ef9de3867bd1f868557ec23d04", size = 656539, upload-time = "2025-11-28T09:34:25.069Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/e9/36b340c7ec01dad6f034481e98fc9fc0133307beb05c714c0542af98bbde/lightning-2.6.0-py3-none-any.whl", hash = "sha256:f1a13a48909960a3454518486f113fae4fadb2db0e28e9c50d8d38d46c9dc3d6", size = 845956, upload-time = "2025-11-28T09:34:23.273Z" }, +] + +[[package]] +name = "lightning-utilities" +version = "0.15.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "setuptools" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b8/39/6fc58ca81492db047149b4b8fd385aa1bfb8c28cd7cacb0c7eb0c44d842f/lightning_utilities-0.15.2.tar.gz", hash = "sha256:cdf12f530214a63dacefd713f180d1ecf5d165338101617b4742e8f22c032e24", size = 31090, upload-time = "2025-08-06T13:57:39.242Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/73/3d757cb3fc16f0f9794dd289bcd0c4a031d9cf54d8137d6b984b2d02edf3/lightning_utilities-0.15.2-py3-none-any.whl", hash = "sha256:ad3ab1703775044bbf880dbf7ddaaac899396c96315f3aa1779cec9d618a9841", size = 29431, upload-time = "2025-08-06T13:57:38.046Z" }, +] + +[[package]] +name = "litellm" +version = "1.90.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "click" }, + { name = "fastuuid" }, + { name = "httpx" }, + { name = "importlib-metadata" }, + { name = "jinja2" }, + { name = "jsonschema" }, + { name = "openai" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "tiktoken" }, + { name = "tokenizers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/24/c6/45b74e44f6605f91cffb2d6ad5463bc743f5636c66a2892c834d90b708e7/litellm-1.90.0.tar.gz", hash = "sha256:55f2be4adfc350ae2931bf369aebf1229aa54ea8ceaa1c13ee65a07724572dae", size = 14815671, upload-time = "2026-06-27T03:12:47.81Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/f7/0864ca487526ed478b857c704244255c7be4e13ed4dde7aeec13f679c904/litellm-1.90.0-py3-none-any.whl", hash = "sha256:3a0fec8405606e34f501544fb9cfe4261fe073bef340d3ca9a60d3fb31c639ee", size = 16607104, upload-time = "2026-06-27T03:12:44.493Z" }, +] + +[[package]] +name = "llvmlite" +version = "0.46.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/cd/08ae687ba099c7e3d21fe2ea536500563ef1943c5105bf6ab4ee3829f68e/llvmlite-0.46.0.tar.gz", hash = "sha256:227c9fd6d09dce2783c18b754b7cd9d9b3b3515210c46acc2d3c5badd9870ceb", size = 193456, upload-time = "2025-12-08T18:15:36.295Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/a1/2ad4b2367915faeebe8447f0a057861f646dbf5fbbb3561db42c65659cf3/llvmlite-0.46.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:82f3d39b16f19aa1a56d5fe625883a6ab600d5cc9ea8906cca70ce94cabba067", size = 37232766, upload-time = "2025-12-08T18:14:48.836Z" }, + { url = "https://files.pythonhosted.org/packages/12/b5/99cf8772fdd846c07da4fd70f07812a3c8fd17ea2409522c946bb0f2b277/llvmlite-0.46.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a3df43900119803bbc52720e758c76f316a9a0f34612a886862dfe0a5591a17e", size = 56275175, upload-time = "2025-12-08T18:14:51.604Z" }, + { url = "https://files.pythonhosted.org/packages/38/f2/ed806f9c003563732da156139c45d970ee435bd0bfa5ed8de87ba972b452/llvmlite-0.46.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de183fefc8022d21b0aa37fc3e90410bc3524aed8617f0ff76732fc6c3af5361", size = 55128630, upload-time = "2025-12-08T18:14:55.107Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/8f5a37a65fc9b7b17408508145edd5f86263ad69c19d3574e818f533a0eb/llvmlite-0.46.0-cp311-cp311-win_amd64.whl", hash = "sha256:e8b10bc585c58bdffec9e0c309bb7d51be1f2f15e169a4b4d42f2389e431eb93", size = 38138652, upload-time = "2025-12-08T18:14:58.171Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f8/4db016a5e547d4e054ff2f3b99203d63a497465f81ab78ec8eb2ff7b2304/llvmlite-0.46.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b9588ad4c63b4f0175a3984b85494f0c927c6b001e3a246a3a7fb3920d9a137", size = 37232767, upload-time = "2025-12-08T18:15:00.737Z" }, + { url = "https://files.pythonhosted.org/packages/aa/85/4890a7c14b4fa54400945cb52ac3cd88545bbdb973c440f98ca41591cdc5/llvmlite-0.46.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3535bd2bb6a2d7ae4012681ac228e5132cdb75fefb1bcb24e33f2f3e0c865ed4", size = 56275176, upload-time = "2025-12-08T18:15:03.936Z" }, + { url = "https://files.pythonhosted.org/packages/6a/07/3d31d39c1a1a08cd5337e78299fca77e6aebc07c059fbd0033e3edfab45c/llvmlite-0.46.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cbfd366e60ff87ea6cc62f50bc4cd800ebb13ed4c149466f50cf2163a473d1e", size = 55128630, upload-time = "2025-12-08T18:15:07.196Z" }, + { url = "https://files.pythonhosted.org/packages/2a/6b/d139535d7590a1bba1ceb68751bef22fadaa5b815bbdf0e858e3875726b2/llvmlite-0.46.0-cp312-cp312-win_amd64.whl", hash = "sha256:398b39db462c39563a97b912d4f2866cd37cba60537975a09679b28fbbc0fb38", size = 38138940, upload-time = "2025-12-08T18:15:10.162Z" }, +] + +[[package]] +name = "mako" +version = "1.3.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +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" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/86/de7e3a1cdcfc941483af70609edc06b83e7c8a0e0dc9ac325200a3f4d220/matplotlib-3.10.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160", size = 8251215, upload-time = "2025-12-10T22:55:16.175Z" }, + { url = "https://files.pythonhosted.org/packages/fd/14/baad3222f424b19ce6ad243c71de1ad9ec6b2e4eb1e458a48fdc6d120401/matplotlib-3.10.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78", size = 8139625, upload-time = "2025-12-10T22:55:17.712Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a0/7024215e95d456de5883e6732e708d8187d9753a21d32f8ddb3befc0c445/matplotlib-3.10.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4", size = 8712614, upload-time = "2025-12-10T22:55:20.8Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f4/b8347351da9a5b3f41e26cf547252d861f685c6867d179a7c9d60ad50189/matplotlib-3.10.8-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2", size = 9540997, upload-time = "2025-12-10T22:55:23.258Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c0/c7b914e297efe0bc36917bf216b2acb91044b91e930e878ae12981e461e5/matplotlib-3.10.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6", size = 9596825, upload-time = "2025-12-10T22:55:25.217Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d3/a4bbc01c237ab710a1f22b4da72f4ff6d77eb4c7735ea9811a94ae239067/matplotlib-3.10.8-cp311-cp311-win_amd64.whl", hash = "sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9", size = 8135090, upload-time = "2025-12-10T22:55:27.162Z" }, + { url = "https://files.pythonhosted.org/packages/89/dd/a0b6588f102beab33ca6f5218b31725216577b2a24172f327eaf6417d5c9/matplotlib-3.10.8-cp311-cp311-win_arm64.whl", hash = "sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2", size = 8012377, upload-time = "2025-12-10T22:55:29.185Z" }, + { url = "https://files.pythonhosted.org/packages/9e/67/f997cdcbb514012eb0d10cd2b4b332667997fb5ebe26b8d41d04962fa0e6/matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", size = 8260453, upload-time = "2025-12-10T22:55:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", size = 8148321, upload-time = "2025-12-10T22:55:33.265Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", size = 8716944, upload-time = "2025-12-10T22:55:34.922Z" }, + { url = "https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", size = 9550099, upload-time = "2025-12-10T22:55:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/57/61/78cd5920d35b29fd2a0fe894de8adf672ff52939d2e9b43cb83cd5ce1bc7/matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", size = 9613040, upload-time = "2025-12-10T22:55:38.715Z" }, + { url = "https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", size = 8142717, upload-time = "2025-12-10T22:55:41.103Z" }, + { url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751, upload-time = "2025-12-10T22:55:42.684Z" }, + { url = "https://files.pythonhosted.org/packages/04/30/3afaa31c757f34b7725ab9d2ba8b48b5e89c2019c003e7d0ead143aabc5a/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1", size = 8249198, upload-time = "2025-12-10T22:56:45.584Z" }, + { url = "https://files.pythonhosted.org/packages/48/2f/6334aec331f57485a642a7c8be03cb286f29111ae71c46c38b363230063c/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a", size = 8136817, upload-time = "2025-12-10T22:56:47.339Z" }, + { url = "https://files.pythonhosted.org/packages/73/e4/6d6f14b2a759c622f191b2d67e9075a3f56aaccb3be4bb9bb6890030d0a0/matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", size = 8713867, upload-time = "2025-12-10T22:56:48.954Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mediaprocesspipeline" +version = "0.4.1" +source = { virtual = "." } +dependencies = [ + { name = "fastapi" }, + { name = "httpx", extra = ["socks"] }, + { name = "litellm" }, + { name = "numpy" }, + { name = "openai" }, + { name = "pillow" }, + { name = "playwright" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "python-dotenv" }, + { name = "python-multipart" }, + { name = "rich" }, + { name = "sqlite-vec" }, + { name = "transformers" }, + { name = "typer" }, + { name = "uvicorn", extra = ["standard"] }, + { name = "youtube-transcript-api" }, + { name = "yt-dlp" }, +] + +[package.optional-dependencies] +asr-api-vad = [ + { name = "onnxruntime" }, +] +dev = [ + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "ruff" }, +] +hf-local-inference = [ + { name = "accelerate" }, + { name = "torch" }, +] +local-asr = [ + { name = "pyannote-audio" }, + { name = "qwen-asr" }, + { name = "torch" }, + { name = "torchaudio" }, +] +local-llm = [ + { name = "accelerate" }, + { name = "torch" }, +] +local-models = [ + { name = "accelerate" }, + { name = "audio-separator", extra = ["gpu"] }, + { name = "pyannote-audio" }, + { name = "qwen-asr" }, + { name = "torch" }, + { name = "torchaudio" }, +] +uvr = [ + { name = "audio-separator", extra = ["gpu"] }, + { name = "torch" }, + { name = "torchaudio" }, +] + +[package.metadata] +requires-dist = [ + { name = "accelerate", marker = "extra == 'hf-local-inference'", specifier = ">=0.34.0" }, + { name = "accelerate", marker = "extra == 'local-llm'", specifier = ">=0.34.0" }, + { name = "accelerate", marker = "extra == 'local-models'", specifier = ">=0.34.0" }, + { name = "audio-separator", extras = ["gpu"], marker = "extra == 'local-models'", specifier = ">=0.41.0" }, + { name = "audio-separator", extras = ["gpu"], marker = "extra == 'uvr'", specifier = ">=0.41.0" }, + { name = "fastapi", specifier = ">=0.115.0" }, + { name = "httpx", extras = ["socks"], specifier = ">=0.28.0" }, + { name = "litellm", specifier = ">=1.83.14" }, + { name = "numpy", specifier = ">=1.26.0" }, + { name = "onnxruntime", marker = "extra == 'asr-api-vad'", specifier = ">=1.16.1" }, + { name = "openai", specifier = ">=1.50.0" }, + { name = "pillow", specifier = ">=12.1.0" }, + { name = "playwright", specifier = ">=1.60.0" }, + { name = "pyannote-audio", marker = "extra == 'local-asr'", specifier = ">=3.4.0" }, + { name = "pyannote-audio", marker = "extra == 'local-models'", specifier = ">=3.4.0" }, + { name = "pydantic", specifier = ">=2.10.0" }, + { name = "pydantic-settings", specifier = ">=2.6.0" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3.0" }, + { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.24.0" }, + { name = "python-dotenv", specifier = ">=1.0.0" }, + { name = "python-multipart", specifier = ">=0.0.21" }, + { name = "qwen-asr", marker = "extra == 'local-asr'", specifier = ">=0.0.6" }, + { name = "qwen-asr", marker = "extra == 'local-models'", specifier = ">=0.0.6" }, + { name = "rich", specifier = ">=13.0.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.8.0" }, + { name = "sqlite-vec", specifier = ">=0.1.6" }, + { name = "torch", marker = "extra == 'hf-local-inference'", specifier = "~=2.8.0", index = "https://download.pytorch.org/whl/cu128" }, + { name = "torch", marker = "extra == 'local-asr'", specifier = "~=2.8.0", index = "https://download.pytorch.org/whl/cu128" }, + { name = "torch", marker = "extra == 'local-llm'", specifier = "~=2.8.0", index = "https://download.pytorch.org/whl/cu128" }, + { name = "torch", marker = "extra == 'local-models'", specifier = "~=2.8.0", index = "https://download.pytorch.org/whl/cu128" }, + { name = "torch", marker = "extra == 'uvr'", specifier = "~=2.8.0", index = "https://download.pytorch.org/whl/cu128" }, + { name = "torchaudio", marker = "extra == 'local-asr'", specifier = "~=2.8.0", index = "https://download.pytorch.org/whl/cu128" }, + { name = "torchaudio", marker = "extra == 'local-models'", specifier = "~=2.8.0", index = "https://download.pytorch.org/whl/cu128" }, + { name = "torchaudio", marker = "extra == 'uvr'", specifier = "~=2.8.0", index = "https://download.pytorch.org/whl/cu128" }, + { name = "transformers", specifier = ">=4.45.0" }, + { name = "typer", specifier = ">=0.15.0" }, + { name = "uvicorn", extras = ["standard"], specifier = ">=0.32.0" }, + { name = "youtube-transcript-api", specifier = ">=1.2.4" }, + { name = "yt-dlp", specifier = ">=2026.3.17" }, +] +provides-extras = ["dev", "asr-api-vad", "uvr", "local-asr", "local-llm", "hf-local-inference", "local-models"] + +[[package]] +name = "ml-collections" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "absl-py" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b8/f8/1a9ae6696dbb6bc9c44ddf5c5e84710d77fe9a35a57e8a06722e1836a4a6/ml_collections-1.1.0.tar.gz", hash = "sha256:0ac1ac6511b9f1566863e0bb0afad0c64e906ea278ad3f4d2144a55322671f6f", size = 61356, upload-time = "2025-04-17T08:25:02.247Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/8a/18d4ff2c7bd83f30d6924bd4ad97abf418488c3f908dea228d6f0961ad68/ml_collections-1.1.0-py3-none-any.whl", hash = "sha256:23b6fa4772aac1ae745a96044b925a5746145a70734f087eaca6626e92c05cbc", size = 76707, upload-time = "2025-04-17T08:24:59.038Z" }, +] + +[[package]] +name = "ml-dtypes" +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/4a/c27b42ed9b1c7d13d9ba8b6905dece787d6259152f2309338aed29b2447b/ml_dtypes-0.5.4.tar.gz", hash = "sha256:8ab06a50fb9bf9666dd0fe5dfb4676fa2b0ac0f31ecff72a6c3af8e22c063453", size = 692314, upload-time = "2025-11-17T22:32:31.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/5e/712092cfe7e5eb667b8ad9ca7c54442f21ed7ca8979745f1000e24cf8737/ml_dtypes-0.5.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6c7ecb74c4bd71db68a6bea1edf8da8c34f3d9fe218f038814fd1d310ac76c90", size = 679734, upload-time = "2025-11-17T22:31:39.223Z" }, + { url = "https://files.pythonhosted.org/packages/4f/cf/912146dfd4b5c0eea956836c01dcd2fce6c9c844b2691f5152aca196ce4f/ml_dtypes-0.5.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc11d7e8c44a65115d05e2ab9989d1e045125d7be8e05a071a48bc76eb6d6040", size = 5056165, upload-time = "2025-11-17T22:31:41.071Z" }, + { url = "https://files.pythonhosted.org/packages/a9/80/19189ea605017473660e43762dc853d2797984b3c7bf30ce656099add30c/ml_dtypes-0.5.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19b9a53598f21e453ea2fbda8aa783c20faff8e1eeb0d7ab899309a0053f1483", size = 5034975, upload-time = "2025-11-17T22:31:42.758Z" }, + { url = "https://files.pythonhosted.org/packages/b4/24/70bd59276883fdd91600ca20040b41efd4902a923283c4d6edcb1de128d2/ml_dtypes-0.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:7c23c54a00ae43edf48d44066a7ec31e05fdc2eee0be2b8b50dd1903a1db94bb", size = 210742, upload-time = "2025-11-17T22:31:44.068Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c9/64230ef14e40aa3f1cb254ef623bf812735e6bec7772848d19131111ac0d/ml_dtypes-0.5.4-cp311-cp311-win_arm64.whl", hash = "sha256:557a31a390b7e9439056644cb80ed0735a6e3e3bb09d67fd5687e4b04238d1de", size = 160709, upload-time = "2025-11-17T22:31:46.557Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b8/3c70881695e056f8a32f8b941126cf78775d9a4d7feba8abcb52cb7b04f2/ml_dtypes-0.5.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a174837a64f5b16cab6f368171a1a03a27936b31699d167684073ff1c4237dac", size = 676927, upload-time = "2025-11-17T22:31:48.182Z" }, + { url = "https://files.pythonhosted.org/packages/54/0f/428ef6881782e5ebb7eca459689448c0394fa0a80bea3aa9262cba5445ea/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7f7c643e8b1320fd958bf098aa7ecf70623a42ec5154e3be3be673f4c34d900", size = 5028464, upload-time = "2025-11-17T22:31:50.135Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cb/28ce52eb94390dda42599c98ea0204d74799e4d8047a0eb559b6fd648056/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ad459e99793fa6e13bd5b7e6792c8f9190b4e5a1b45c63aba14a4d0a7f1d5ff", size = 5009002, upload-time = "2025-11-17T22:31:52.001Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f0/0cfadd537c5470378b1b32bd859cf2824972174b51b873c9d95cfd7475a5/ml_dtypes-0.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:c1a953995cccb9e25a4ae19e34316671e4e2edaebe4cf538229b1fc7109087b7", size = 212222, upload-time = "2025-11-17T22:31:53.742Z" }, + { url = "https://files.pythonhosted.org/packages/16/2e/9acc86985bfad8f2c2d30291b27cd2bb4c74cea08695bd540906ed744249/ml_dtypes-0.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:9bad06436568442575beb2d03389aa7456c690a5b05892c471215bfd8cf39460", size = 160793, upload-time = "2025-11-17T22:31:55.358Z" }, +] + +[[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.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/97/560d11202bcd537abca693fd85d81cebe2107ba17301de42b01ac1677b69/msgpack-1.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2e86a607e558d22985d856948c12a3fa7b42efad264dca8a3ebbcfa2735d786c", size = 82271, upload-time = "2025-10-08T09:14:49.967Z" }, + { url = "https://files.pythonhosted.org/packages/83/04/28a41024ccbd67467380b6fb440ae916c1e4f25e2cd4c63abe6835ac566e/msgpack-1.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0", size = 84914, upload-time = "2025-10-08T09:14:50.958Z" }, + { url = "https://files.pythonhosted.org/packages/71/46/b817349db6886d79e57a966346cf0902a426375aadc1e8e7a86a75e22f19/msgpack-1.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61c8aa3bd513d87c72ed0b37b53dd5c5a0f58f2ff9f26e1555d3bd7948fb7296", size = 416962, upload-time = "2025-10-08T09:14:51.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/e0/6cc2e852837cd6086fe7d8406af4294e66827a60a4cf60b86575a4a65ca8/msgpack-1.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef", size = 426183, upload-time = "2025-10-08T09:14:53.477Z" }, + { url = "https://files.pythonhosted.org/packages/25/98/6a19f030b3d2ea906696cedd1eb251708e50a5891d0978b012cb6107234c/msgpack-1.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7bc8813f88417599564fafa59fd6f95be417179f76b40325b500b3c98409757c", size = 411454, upload-time = "2025-10-08T09:14:54.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/cd/9098fcb6adb32187a70b7ecaabf6339da50553351558f37600e53a4a2a23/msgpack-1.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bafca952dc13907bdfdedfc6a5f579bf4f292bdd506fadb38389afa3ac5b208e", size = 422341, upload-time = "2025-10-08T09:14:56.328Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ae/270cecbcf36c1dc85ec086b33a51a4d7d08fc4f404bdbc15b582255d05ff/msgpack-1.1.2-cp311-cp311-win32.whl", hash = "sha256:602b6740e95ffc55bfb078172d279de3773d7b7db1f703b2f1323566b878b90e", size = 64747, upload-time = "2025-10-08T09:14:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/2a/79/309d0e637f6f37e83c711f547308b91af02b72d2326ddd860b966080ef29/msgpack-1.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:d198d275222dc54244bf3327eb8cbe00307d220241d9cec4d306d49a44e85f68", size = 71633, upload-time = "2025-10-08T09:14:59.177Z" }, + { url = "https://files.pythonhosted.org/packages/73/4d/7c4e2b3d9b1106cd0aa6cb56cc57c6267f59fa8bfab7d91df5adc802c847/msgpack-1.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:86f8136dfa5c116365a8a651a7d7484b65b13339731dd6faebb9a0242151c406", size = 64755, upload-time = "2025-10-08T09:15:00.48Z" }, + { url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" }, + { url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" }, + { url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" }, + { url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" }, + { url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" }, + { url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" }, + { url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834, upload-time = "2025-10-06T14:52:30.657Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/9e/5c727587644d67b2ed479041e4b1c58e30afc011e3d45d25bbe35781217c/multidict-6.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc", size = 76604, upload-time = "2025-10-06T14:48:54.277Z" }, + { url = "https://files.pythonhosted.org/packages/17/e4/67b5c27bd17c085a5ea8f1ec05b8a3e5cba0ca734bfcad5560fb129e70ca/multidict-6.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721", size = 44715, upload-time = "2025-10-06T14:48:55.445Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e1/866a5d77be6ea435711bef2a4291eed11032679b6b28b56b4776ab06ba3e/multidict-6.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6", size = 44332, upload-time = "2025-10-06T14:48:56.706Z" }, + { url = "https://files.pythonhosted.org/packages/31/61/0c2d50241ada71ff61a79518db85ada85fdabfcf395d5968dae1cbda04e5/multidict-6.7.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c", size = 245212, upload-time = "2025-10-06T14:48:58.042Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e0/919666a4e4b57fff1b57f279be1c9316e6cdc5de8a8b525d76f6598fefc7/multidict-6.7.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7", size = 246671, upload-time = "2025-10-06T14:49:00.004Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cc/d027d9c5a520f3321b65adea289b965e7bcbd2c34402663f482648c716ce/multidict-6.7.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7", size = 225491, upload-time = "2025-10-06T14:49:01.393Z" }, + { url = "https://files.pythonhosted.org/packages/75/c4/bbd633980ce6155a28ff04e6a6492dd3335858394d7bb752d8b108708558/multidict-6.7.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9", size = 257322, upload-time = "2025-10-06T14:49:02.745Z" }, + { url = "https://files.pythonhosted.org/packages/4c/6d/d622322d344f1f053eae47e033b0b3f965af01212de21b10bcf91be991fb/multidict-6.7.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8", size = 254694, upload-time = "2025-10-06T14:49:04.15Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9f/78f8761c2705d4c6d7516faed63c0ebdac569f6db1bef95e0d5218fdc146/multidict-6.7.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd", size = 246715, upload-time = "2025-10-06T14:49:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/950818e04f91b9c2b95aab3d923d9eabd01689d0dcd889563988e9ea0fd8/multidict-6.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb", size = 243189, upload-time = "2025-10-06T14:49:07.37Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3d/77c79e1934cad2ee74991840f8a0110966d9599b3af95964c0cd79bb905b/multidict-6.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6", size = 237845, upload-time = "2025-10-06T14:49:08.759Z" }, + { url = "https://files.pythonhosted.org/packages/63/1b/834ce32a0a97a3b70f86437f685f880136677ac00d8bce0027e9fd9c2db7/multidict-6.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2", size = 246374, upload-time = "2025-10-06T14:49:10.574Z" }, + { url = "https://files.pythonhosted.org/packages/23/ef/43d1c3ba205b5dec93dc97f3fba179dfa47910fc73aaaea4f7ceb41cec2a/multidict-6.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff", size = 253345, upload-time = "2025-10-06T14:49:12.331Z" }, + { url = "https://files.pythonhosted.org/packages/6b/03/eaf95bcc2d19ead522001f6a650ef32811aa9e3624ff0ad37c445c7a588c/multidict-6.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b", size = 246940, upload-time = "2025-10-06T14:49:13.821Z" }, + { url = "https://files.pythonhosted.org/packages/e8/df/ec8a5fd66ea6cd6f525b1fcbb23511b033c3e9bc42b81384834ffa484a62/multidict-6.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34", size = 242229, upload-time = "2025-10-06T14:49:15.603Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a2/59b405d59fd39ec86d1142630e9049243015a5f5291ba49cadf3c090c541/multidict-6.7.0-cp311-cp311-win32.whl", hash = "sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff", size = 41308, upload-time = "2025-10-06T14:49:16.871Z" }, + { url = "https://files.pythonhosted.org/packages/32/0f/13228f26f8b882c34da36efa776c3b7348455ec383bab4a66390e42963ae/multidict-6.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81", size = 46037, upload-time = "2025-10-06T14:49:18.457Z" }, + { url = "https://files.pythonhosted.org/packages/84/1f/68588e31b000535a3207fd3c909ebeec4fb36b52c442107499c18a896a2a/multidict-6.7.0-cp311-cp311-win_arm64.whl", hash = "sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912", size = 43023, upload-time = "2025-10-06T14:49:19.648Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9e/9f61ac18d9c8b475889f32ccfa91c9f59363480613fc807b6e3023d6f60b/multidict-6.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184", size = 76877, upload-time = "2025-10-06T14:49:20.884Z" }, + { url = "https://files.pythonhosted.org/packages/38/6f/614f09a04e6184f8824268fce4bc925e9849edfa654ddd59f0b64508c595/multidict-6.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45", size = 45467, upload-time = "2025-10-06T14:49:22.054Z" }, + { url = "https://files.pythonhosted.org/packages/b3/93/c4f67a436dd026f2e780c433277fff72be79152894d9fc36f44569cab1a6/multidict-6.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa", size = 43834, upload-time = "2025-10-06T14:49:23.566Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f5/013798161ca665e4a422afbc5e2d9e4070142a9ff8905e482139cd09e4d0/multidict-6.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7", size = 250545, upload-time = "2025-10-06T14:49:24.882Z" }, + { url = "https://files.pythonhosted.org/packages/71/2f/91dbac13e0ba94669ea5119ba267c9a832f0cb65419aca75549fcf09a3dc/multidict-6.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e", size = 258305, upload-time = "2025-10-06T14:49:26.778Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b0/754038b26f6e04488b48ac621f779c341338d78503fb45403755af2df477/multidict-6.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546", size = 242363, upload-time = "2025-10-06T14:49:28.562Z" }, + { url = "https://files.pythonhosted.org/packages/87/15/9da40b9336a7c9fa606c4cf2ed80a649dffeb42b905d4f63a1d7eb17d746/multidict-6.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4", size = 268375, upload-time = "2025-10-06T14:49:29.96Z" }, + { url = "https://files.pythonhosted.org/packages/82/72/c53fcade0cc94dfaad583105fd92b3a783af2091eddcb41a6d5a52474000/multidict-6.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1", size = 269346, upload-time = "2025-10-06T14:49:31.404Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e2/9baffdae21a76f77ef8447f1a05a96ec4bc0a24dae08767abc0a2fe680b8/multidict-6.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d", size = 256107, upload-time = "2025-10-06T14:49:32.974Z" }, + { url = "https://files.pythonhosted.org/packages/3c/06/3f06f611087dc60d65ef775f1fb5aca7c6d61c6db4990e7cda0cef9b1651/multidict-6.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304", size = 253592, upload-time = "2025-10-06T14:49:34.52Z" }, + { url = "https://files.pythonhosted.org/packages/20/24/54e804ec7945b6023b340c412ce9c3f81e91b3bf5fa5ce65558740141bee/multidict-6.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12", size = 251024, upload-time = "2025-10-06T14:49:35.956Z" }, + { url = "https://files.pythonhosted.org/packages/14/48/011cba467ea0b17ceb938315d219391d3e421dfd35928e5dbdc3f4ae76ef/multidict-6.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62", size = 251484, upload-time = "2025-10-06T14:49:37.631Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2f/919258b43bb35b99fa127435cfb2d91798eb3a943396631ef43e3720dcf4/multidict-6.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0", size = 263579, upload-time = "2025-10-06T14:49:39.502Z" }, + { url = "https://files.pythonhosted.org/packages/31/22/a0e884d86b5242b5a74cf08e876bdf299e413016b66e55511f7a804a366e/multidict-6.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a", size = 259654, upload-time = "2025-10-06T14:49:41.32Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/17e10e1b5c5f5a40f2fcbb45953c9b215f8a4098003915e46a93f5fcaa8f/multidict-6.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8", size = 251511, upload-time = "2025-10-06T14:49:46.021Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9a/201bb1e17e7af53139597069c375e7b0dcbd47594604f65c2d5359508566/multidict-6.7.0-cp312-cp312-win32.whl", hash = "sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4", size = 41895, upload-time = "2025-10-06T14:49:48.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/e2/348cd32faad84eaf1d20cce80e2bb0ef8d312c55bca1f7fa9865e7770aaf/multidict-6.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b", size = 46073, upload-time = "2025-10-06T14:49:50.28Z" }, + { url = "https://files.pythonhosted.org/packages/25/ec/aad2613c1910dce907480e0c3aa306905830f25df2e54ccc9dea450cb5aa/multidict-6.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec", size = 43226, upload-time = "2025-10-06T14:49:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317, upload-time = "2025-10-06T14:52:29.272Z" }, +] + +[[package]] +name = "nagisa" +version = "0.2.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dynet38" }, + { name = "numpy" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/71/1cc63f4fde1c1ca491ea69b46c3c5bd6bb06f3efa8262f34b227fe25dbf5/nagisa-0.2.11.tar.gz", hash = "sha256:213e8f837c6f7391ea1f56f4fa6047612d117e3f0e9c4f6b93a3a77f78ba6e6f", size = 20930765, upload-time = "2024-01-28T17:32:49.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/94/49f86e8b4573de54ec6e75afc75922d42e6b2816da176b35b4be1d734604/nagisa-0.2.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:41db2de9a2c39b7ea64deb8d89073e9dc2303157e3775fa03f6b17d872be3386", size = 21351995, upload-time = "2024-01-28T17:29:18.208Z" }, + { url = "https://files.pythonhosted.org/packages/20/bc/44fad656edb2dd0fb5b995d828c7889ac9300c804f1fa4f3396e5272e015/nagisa-0.2.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:14ad851d6281671fdfcafd41ff276c3a46610c2f0ec53988561de3ab10037e62", size = 21348141, upload-time = "2024-06-14T15:42:58.266Z" }, + { url = "https://files.pythonhosted.org/packages/ee/c3/6d75e857824c9c6dbac86985efe6ec89b8cba5b2060d76647a1e6ae4fdf5/nagisa-0.2.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53a9b7dbe925db6a8e9347f85718a72d4ebed8ffe2542af709229b48e505f965", size = 21671530, upload-time = "2024-06-16T11:47:35.451Z" }, + { url = "https://files.pythonhosted.org/packages/d8/95/887739ce576235371597dd39b009f2f6a50d6b06a7bc281f89b412283edf/nagisa-0.2.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aff78f59e9d206cb391dbbedf18866b012514cc710138f3ffd660ebbefcc8122", size = 21657823, upload-time = "2024-01-28T17:29:26.147Z" }, + { url = "https://files.pythonhosted.org/packages/71/e8/72c93208e9301d614d3cd190e495ee11321b1fe42165a3e2128ac8b9d1a9/nagisa-0.2.11-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70d715680e33c48a8066a8588a52149d98644cddae1b5127cc5660c6418c81ce", size = 21678750, upload-time = "2024-01-28T17:29:32.458Z" }, + { url = "https://files.pythonhosted.org/packages/8b/78/97b92fab0cffc74613485cb6cdc57a20f1ff173947e1b5a416d237e4b200/nagisa-0.2.11-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:58a07030de36e59af394c16b7e7028a3a9d5df193470d1620fb1805d02e2605d", size = 21656797, upload-time = "2024-01-28T17:29:38.458Z" }, + { url = "https://files.pythonhosted.org/packages/87/10/479edb36afd987c3e15b3279a0a5d0aa68e5915c0104052fa7965eadb0cc/nagisa-0.2.11-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ed1e1c700c4785bd576d670e650f2093d10606c41018e953986f0f0a39fa994", size = 21675016, upload-time = "2024-01-28T17:29:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/59/cf/d6f568799c6bd945abe14277ead0b9c9a9d5bffe3bb05584a8b07450fb98/nagisa-0.2.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:da45c9977ee53b53a8a834e11515f74f7c2bb6fca3e1ca1d348993fe9b34b2f9", size = 21673993, upload-time = "2024-06-16T11:47:42.971Z" }, + { url = "https://files.pythonhosted.org/packages/0f/07/78c9b8f676f19f7714b15722dd512058c6b82b4627db35ceeb4ccb82475f/nagisa-0.2.11-cp311-cp311-win_amd64.whl", hash = "sha256:551f8060e0208bbc80ab8766f923568b2cdbe49d47012cc0d29a310de0bd3a32", size = 21354044, upload-time = "2025-12-29T12:15:43.315Z" }, + { url = "https://files.pythonhosted.org/packages/44/05/7cec93b078822641e600a010bede2a323d7fdf682e1c9cea14db2fc0feaa/nagisa-0.2.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aa45b738dffb76dbdea7c80e1b902bf69f73fbc12aac8d5df1c6a4297cb9f004", size = 21364577, upload-time = "2025-12-30T19:12:16.664Z" }, + { url = "https://files.pythonhosted.org/packages/54/69/ed7599a07e69e7a63291d409972996e3df2bcb66bcd78cf76c219283cf6b/nagisa-0.2.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:02013feb15a5adcc39883ba4e5ffd0a7484de23d77a14642f40a6f12b500d485", size = 21348603, upload-time = "2024-06-14T15:43:03.647Z" }, + { url = "https://files.pythonhosted.org/packages/6b/0a/ab358b5a1130b82db58d9fccc368ba43021a528d2d26400cee88d668d6b9/nagisa-0.2.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f81281a5460ec5bb95dfd0d062ae68b7512d247f2df08c5101207ce1925712f3", size = 21649303, upload-time = "2024-06-16T11:47:49.485Z" }, + { url = "https://files.pythonhosted.org/packages/a0/31/0f19a51a71d5948f12542f0285f81bc5d48793de139f58256d8357bfd545/nagisa-0.2.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:083a5053204754cfcef65766ff24faa6033c04013698d817c74367f987feca96", size = 21639686, upload-time = "2024-01-28T17:29:58.427Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ea/3467d0b2362996eb079ea49d58e7708bd174c04b0e29672bb50b1c984d21/nagisa-0.2.11-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43f2a5a30aa4b5c3f9018062e4c892efb6c18ae80f0d67a9a0611aa3d260dc37", size = 21657127, upload-time = "2024-01-28T17:30:03.871Z" }, + { url = "https://files.pythonhosted.org/packages/9e/db/c31e916b44f63d1e904cb98fcc0385e65c5f7da8f9c68fc4300fb4a1cb14/nagisa-0.2.11-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:716beae15479b09de1482bf4bc8cf8a20df8586a01850cf35eb5450c3edea51f", size = 21637318, upload-time = "2024-01-28T17:30:10.14Z" }, + { url = "https://files.pythonhosted.org/packages/31/7a/10bf406cdc83831614311c785be077292d299dec3d51657ec219a4144593/nagisa-0.2.11-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:128510d25293e1ce40bb62da8e4a35a69eec7076ff498c1e3ee82178dcf4b3e6", size = 21655942, upload-time = "2024-01-28T17:30:19.129Z" }, + { url = "https://files.pythonhosted.org/packages/c2/55/db036126a0b0f82d745fdef6f967ce57b4f7f9a98e188a4904de03deec70/nagisa-0.2.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b5975df0849a2b3774af6bbeef9afa8f95e84df48a9b6097d6367aa2c9ca2c2b", size = 21653414, upload-time = "2024-06-16T11:47:56.533Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f5/6c6206fad84dca34e4383994fab176bd99f9293d731fc3e72bd7da0ab74c/nagisa-0.2.11-cp312-cp312-win_amd64.whl", hash = "sha256:6d68ee11ee6e82a37e54ad10885c120ac5e69446fe635d65327c4931b281d996", size = 21352973, upload-time = "2025-12-29T12:15:50.156Z" }, +] + +[[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.63.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llvmlite" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/60/0145d479b2209bd8fdae5f44201eceb8ce5a23e0ed54c71f57db24618665/numba-0.63.1.tar.gz", hash = "sha256:b320aa675d0e3b17b40364935ea52a7b1c670c9037c39cf92c49502a75902f4b", size = 2761666, upload-time = "2025-12-10T02:57:39.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/90/5f8614c165d2e256fbc6c57028519db6f32e4982475a372bbe550ea0454c/numba-0.63.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b33db00f18ccc790ee9911ce03fcdfe9d5124637d1ecc266f5ae0df06e02fec3", size = 2680501, upload-time = "2025-12-10T02:57:09.797Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9d/d0afc4cf915edd8eadd9b2ab5b696242886ee4f97720d9322650d66a88c6/numba-0.63.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d31ea186a78a7c0f6b1b2a3fe68057fdb291b045c52d86232b5383b6cf4fc25", size = 3744945, upload-time = "2025-12-10T02:57:11.697Z" }, + { url = "https://files.pythonhosted.org/packages/05/a9/d82f38f2ab73f3be6f838a826b545b80339762ee8969c16a8bf1d39395a8/numba-0.63.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed3bb2fbdb651d6aac394388130a7001aab6f4541837123a4b4ab8b02716530c", size = 3450827, upload-time = "2025-12-10T02:57:13.709Z" }, + { url = "https://files.pythonhosted.org/packages/18/3f/a9b106e93c5bd7434e65f044bae0d204e20aa7f7f85d72ceb872c7c04216/numba-0.63.1-cp311-cp311-win_amd64.whl", hash = "sha256:1ecbff7688f044b1601be70113e2fb1835367ee0b28ffa8f3adf3a05418c5c87", size = 2747262, upload-time = "2025-12-10T02:57:15.664Z" }, + { url = "https://files.pythonhosted.org/packages/14/9c/c0974cd3d00ff70d30e8ff90522ba5fbb2bcee168a867d2321d8d0457676/numba-0.63.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2819cd52afa5d8d04e057bdfd54367575105f8829350d8fb5e4066fb7591cc71", size = 2680981, upload-time = "2025-12-10T02:57:17.579Z" }, + { url = "https://files.pythonhosted.org/packages/cb/70/ea2bc45205f206b7a24ee68a159f5097c9ca7e6466806e7c213587e0c2b1/numba-0.63.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5cfd45dbd3d409e713b1ccfdc2ee72ca82006860254429f4ef01867fdba5845f", size = 3801656, upload-time = "2025-12-10T02:57:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/0d/82/4f4ba4fd0f99825cbf3cdefd682ca3678be1702b63362011de6e5f71f831/numba-0.63.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69a599df6976c03b7ecf15d05302696f79f7e6d10d620367407517943355bcb0", size = 3501857, upload-time = "2025-12-10T02:57:20.721Z" }, + { url = "https://files.pythonhosted.org/packages/af/fd/6540456efa90b5f6604a86ff50dabefb187e43557e9081adcad3be44f048/numba-0.63.1-cp312-cp312-win_amd64.whl", hash = "sha256:bbad8c63e4fc7eb3cdb2c2da52178e180419f7969f9a685f283b313a70b92af3", size = 2750282, upload-time = "2025-12-10T02:57:22.474Z" }, +] + +[[package]] +name = "numpy" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/75/10dd1f8116a8b796cb2c737b674e02d02e80454bda953fa7e65d8c12b016/numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78", size = 18902015, upload-time = "2024-08-26T20:19:40.945Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/cf/034500fb83041aa0286e0fb16e7c76e5c8b67c0711bb6e9e9737a717d5fe/numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448", size = 21169137, upload-time = "2024-08-26T20:07:45.345Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d9/32de45561811a4b87fbdee23b5797394e3d1504b4a7cf40c10199848893e/numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195", size = 13703552, upload-time = "2024-08-26T20:08:06.666Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ca/2f384720020c7b244d22508cb7ab23d95f179fcfff33c31a6eeba8d6c512/numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57", size = 5298957, upload-time = "2024-08-26T20:08:15.83Z" }, + { url = "https://files.pythonhosted.org/packages/0e/78/a3e4f9fb6aa4e6fdca0c5428e8ba039408514388cf62d89651aade838269/numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a", size = 6905573, upload-time = "2024-08-26T20:08:27.185Z" }, + { url = "https://files.pythonhosted.org/packages/a0/72/cfc3a1beb2caf4efc9d0b38a15fe34025230da27e1c08cc2eb9bfb1c7231/numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669", size = 13914330, upload-time = "2024-08-26T20:08:48.058Z" }, + { url = "https://files.pythonhosted.org/packages/ba/a8/c17acf65a931ce551fee11b72e8de63bf7e8a6f0e21add4c937c83563538/numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951", size = 19534895, upload-time = "2024-08-26T20:09:16.536Z" }, + { url = "https://files.pythonhosted.org/packages/ba/86/8767f3d54f6ae0165749f84648da9dcc8cd78ab65d415494962c86fac80f/numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9", size = 19937253, upload-time = "2024-08-26T20:09:46.263Z" }, + { url = "https://files.pythonhosted.org/packages/df/87/f76450e6e1c14e5bb1eae6836478b1028e096fd02e85c1c37674606ab752/numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15", size = 14414074, upload-time = "2024-08-26T20:10:08.483Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/0f0f328e1e59f73754f06e1adfb909de43726d4f24c6a3f8805f34f2b0fa/numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4", size = 6470640, upload-time = "2024-08-26T20:10:19.732Z" }, + { url = "https://files.pythonhosted.org/packages/eb/57/3a3f14d3a759dcf9bf6e9eda905794726b758819df4663f217d658a58695/numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc", size = 15910230, upload-time = "2024-08-26T20:10:43.413Z" }, + { url = "https://files.pythonhosted.org/packages/45/40/2e117be60ec50d98fa08c2f8c48e09b3edea93cfcabd5a9ff6925d54b1c2/numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b", size = 20895803, upload-time = "2024-08-26T20:11:13.916Z" }, + { url = "https://files.pythonhosted.org/packages/46/92/1b8b8dee833f53cef3e0a3f69b2374467789e0bb7399689582314df02651/numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e", size = 13471835, upload-time = "2024-08-26T20:11:34.779Z" }, + { url = "https://files.pythonhosted.org/packages/7f/19/e2793bde475f1edaea6945be141aef6c8b4c669b90c90a300a8954d08f0a/numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c", size = 5038499, upload-time = "2024-08-26T20:11:43.902Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ff/ddf6dac2ff0dd50a7327bcdba45cb0264d0e96bb44d33324853f781a8f3c/numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c", size = 6633497, upload-time = "2024-08-26T20:11:55.09Z" }, + { url = "https://files.pythonhosted.org/packages/72/21/67f36eac8e2d2cd652a2e69595a54128297cdcb1ff3931cfc87838874bd4/numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692", size = 13621158, upload-time = "2024-08-26T20:12:14.95Z" }, + { url = "https://files.pythonhosted.org/packages/39/68/e9f1126d757653496dbc096cb429014347a36b228f5a991dae2c6b6cfd40/numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a", size = 19236173, upload-time = "2024-08-26T20:12:44.049Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e9/1f5333281e4ebf483ba1c888b1d61ba7e78d7e910fdd8e6499667041cc35/numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c", size = 19634174, upload-time = "2024-08-26T20:13:13.634Z" }, + { url = "https://files.pythonhosted.org/packages/71/af/a469674070c8d8408384e3012e064299f7a2de540738a8e414dcfd639996/numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded", size = 14099701, upload-time = "2024-08-26T20:13:34.851Z" }, + { url = "https://files.pythonhosted.org/packages/d0/3d/08ea9f239d0e0e939b6ca52ad403c84a2bce1bde301a8eb4888c1c1543f1/numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5", size = 6174313, upload-time = "2024-08-26T20:13:45.653Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b5/4ac39baebf1fdb2e72585c8352c56d063b6126be9fc95bd2bb5ef5770c20/numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a", size = 15606179, upload-time = "2024-08-26T20:14:08.786Z" }, +] + +[[package]] +name = "nvidia-cublas-cu12" +version = "12.8.4.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/61/e24b560ab2e2eaeb3c839129175fb330dfcfc29e5203196e5541a4c44682/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142", size = 594346921, upload-time = "2025-03-07T01:44:31.254Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/02/2adcaa145158bf1a8295d83591d22e4103dbfd821bcaf6f3f53151ca4ffa/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182", size = 10248621, upload-time = "2025-03-07T01:40:21.213Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/6b/32f747947df2da6994e999492ab306a903659555dddc0fbdeb9d71f75e52/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994", size = 88040029, upload-time = "2025-03-07T01:42:13.562Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/9b/a997b638fcd068ad6e4d53b8551a7d30fe8b404d6f1804abf1df69838932/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90", size = 954765, upload-time = "2025-03-07T01:40:01.615Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu12" +version = "9.10.2.21" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12", marker = "platform_machine != 's390x'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" }, +] + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.3.3.83" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine != 's390x'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" }, +] + +[[package]] +name = "nvidia-cufile-cu12" +version = "1.13.1.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/fe/1bcba1dfbfb8d01be8d93f07bfc502c93fa23afa6fd5ab3fc7c1df71038a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc", size = 1197834, upload-time = "2025-03-07T01:45:50.723Z" }, +] + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.9.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/aa/6584b56dc84ebe9cf93226a5cde4d99080c8e90ab40f0c27bda7a0f29aa1/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9", size = 63619976, upload-time = "2025-03-07T01:46:23.323Z" }, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.7.3.90" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12", marker = "platform_machine != 's390x'" }, + { name = "nvidia-cusparse-cu12", marker = "platform_machine != 's390x'" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine != 's390x'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" }, +] + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.5.8.93" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine != 's390x'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu12" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/79/12978b96bd44274fe38b5dde5cfb660b1d114f70a65ef962bcbbed99b549/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623", size = 287193691, upload-time = "2025-02-26T00:15:44.104Z" }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.27.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/5b/4e4fff7bad39adf89f735f2bc87248c81db71205b62bcc0d5ca5b606b3c3/nvidia_nccl_cu12-2.27.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adf27ccf4238253e0b826bce3ff5fa532d65fc42322c8bfdfaf28024c0fbe039", size = 322364134, upload-time = "2025-06-03T21:58:04.013Z" }, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/74/86a07f1d0f42998ca31312f998bd3b9a7eff7f52378f4f270c8679c77fb9/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88", size = 39254836, upload-time = "2025-03-07T01:49:55.661Z" }, +] + +[[package]] +name = "nvidia-nvtx-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/eb/86626c1bbc2edb86323022371c39aa48df6fd8b0a1647bc274577f72e90b/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f", size = 89954, upload-time = "2025-03-07T01:42:44.131Z" }, +] + +[[package]] +name = "omegaconf" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "antlr4-python3-runtime" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/48/6388f1bb9da707110532cb70ec4d2822858ddfb44f1cdf1233c20a80ea4b/omegaconf-2.3.0.tar.gz", hash = "sha256:d5d4b6d29955cc50ad50c46dc269bcd92c6e00f5f90d23ab5fee7bfca4ba4cc7", size = 3298120, upload-time = "2022-12-08T20:59:22.753Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/94/1843518e420fa3ed6919835845df698c7e27e183cb997394e4a670973a65/omegaconf-2.3.0-py3-none-any.whl", hash = "sha256:7b4df175cdb08ba400f45cae3bdcae7ba8365db4d165fc65fd04b050ab63b46b", size = 79500, upload-time = "2022-12-08T20:59:19.686Z" }, +] + +[[package]] +name = "onnx-weekly" +version = "1.23.0.dev20260629" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ml-dtypes" }, + { name = "numpy" }, + { name = "protobuf" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/96/7433906c73e636f804dfe7bae38449e819006258e7c333d3a51084c1ac60/onnx_weekly-1.23.0.dev20260629.tar.gz", hash = "sha256:7f275f68004d35a6bba4dbe5d22b794169c65da964b3e0e7cd0029a3601f52b1", size = 12244668, upload-time = "2026-06-29T01:14:17.077Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/87/8e06e2e556fc95b20b793afaaff4d90b83232c7d4960d32d10c3123fbd65/onnx_weekly-1.23.0.dev20260629-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:a69fa023e1c0e7f29db83ee69e2f21e4f89ddb3e84321e739cab24f51bb32877", size = 20355800, upload-time = "2026-06-29T01:13:00.55Z" }, + { url = "https://files.pythonhosted.org/packages/07/84/3774d9ec40d149fe19da4c3f23e0552e89617caec3629eb3b33a1360a20d/onnx_weekly-1.23.0.dev20260629-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0b119e7306a8b1106a5e50384d26a6257d200aa4c1d4b6a5a89f3a8a8dd9038", size = 19100745, upload-time = "2026-06-29T01:13:04.986Z" }, + { url = "https://files.pythonhosted.org/packages/49/8f/054ba958dce737446134ee27d58e10b184c9b2a6a1e5589dbbfc3bf6bdd5/onnx_weekly-1.23.0.dev20260629-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c4f66ac912f7b39420967145c2daa711b742435c993fe340467fe8d25ff07513", size = 19323609, upload-time = "2026-06-29T01:13:09.3Z" }, + { url = "https://files.pythonhosted.org/packages/17/af/aeeeec3d462fe6e5dfd711cc97df21f9f5e9bdc63ba9862e20d21a8ae156/onnx_weekly-1.23.0.dev20260629-cp311-cp311-win32.whl", hash = "sha256:d407a02b14bafdaa22b562f35084fb2c3ea767a0f2d1502ee91122c3042e05b9", size = 18285173, upload-time = "2026-06-29T01:13:13.41Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d8/c24af2c23e740d047e3ac9b3ec84c96a19da4888b620747d83a2d0cec88b/onnx_weekly-1.23.0.dev20260629-cp311-cp311-win_amd64.whl", hash = "sha256:73b9bdf43724f46d196acfe2d2bf6e855dfb04ea32923a919060b3c9ae4c70ea", size = 18415774, upload-time = "2026-06-29T01:13:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ed/aaebd3fc478ea0630b1250208b065a9b596d913d67114564250dcddc1ab5/onnx_weekly-1.23.0.dev20260629-cp311-cp311-win_arm64.whl", hash = "sha256:efb1ab8ff205a98b38313762e00c8db58624fee3a0bb1f0771bd30c1e34a25f7", size = 18584702, upload-time = "2026-06-29T01:13:22.069Z" }, + { url = "https://files.pythonhosted.org/packages/b8/50/51b3ba1b6b133fcf3736d8f5af0786595c4646f0245790956dbeb11eeabd/onnx_weekly-1.23.0.dev20260629-cp312-abi3-macosx_12_0_universal2.whl", hash = "sha256:5d26273613e4d32dddc0b89b21664be0049b74ef5bcd0a788331731a903fe9b0", size = 20356662, upload-time = "2026-06-29T01:13:26.615Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f2/914474cea73bcebaacc96a667830626cdf2dae7f10fbf04f5281e1ea371f/onnx_weekly-1.23.0.dev20260629-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9235745c9aaab9e7c9c864ae6b337221cbb7bb15d164ece46409d79027e9c625", size = 19096993, upload-time = "2026-06-29T01:13:30.769Z" }, + { url = "https://files.pythonhosted.org/packages/d8/a6/9ded70f73b22d4672a29c31a963ad64447555c2fb895af99ad8457e87986/onnx_weekly-1.23.0.dev20260629-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b5a1adaa122364e69827c8bd92c4005baa7d12024c18e0ca88a0e4349c51b51", size = 19319075, upload-time = "2026-06-29T01:13:35.196Z" }, + { url = "https://files.pythonhosted.org/packages/2f/74/ca7830ea70277a6599ed4da1a318e550a774ad780cab554e06671c212a14/onnx_weekly-1.23.0.dev20260629-cp312-abi3-pyemscripten_2025_0_wasm32.whl", hash = "sha256:5efc89cdca5486317f5ca24655e23010a66a85a85f3b9caf6f56bff6d1cc09b9", size = 17128035, upload-time = "2026-06-29T01:13:39.56Z" }, + { url = "https://files.pythonhosted.org/packages/48/f9/1c1ab4d559dbd1b77af82477703005e53737a088a6cc07d2a35b4b473f5b/onnx_weekly-1.23.0.dev20260629-cp312-abi3-win32.whl", hash = "sha256:d01496917f1d1ccd28540bed8133e5e51888b43c8cce9f25f700d3af7b200edc", size = 18282215, upload-time = "2026-06-29T01:13:43.483Z" }, + { url = "https://files.pythonhosted.org/packages/04/cd/6c9cb3a020c7eb1c93fd0511c2158ca4f87c0bb2945ddc02a5c1be954e99/onnx_weekly-1.23.0.dev20260629-cp312-abi3-win_amd64.whl", hash = "sha256:cf8b6696c7d1908b53cbd4681afc18644a3d88176b1b0e37bb9f9005d61d9074", size = 18414523, upload-time = "2026-06-29T01:13:47.544Z" }, + { url = "https://files.pythonhosted.org/packages/de/1f/f236379e7e02fadf734cc70db85ca360264d3c6824d61638857926963ad1/onnx_weekly-1.23.0.dev20260629-cp312-abi3-win_arm64.whl", hash = "sha256:8a26a06f2d16acab6782fc43063839dec90187f2e699f5c51fe4e313bfd7185f", size = 18582783, upload-time = "2026-06-29T01:13:51.708Z" }, +] + +[[package]] +name = "onnx2torch-py313" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "onnx-weekly" }, + { name = "torch" }, + { name = "torchvision" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/bb/6301c1ea4c8f8859aff60d2d58a038fd68f9cdb507d1c60776d4e3b93b1f/onnx2torch_py313-1.6.0.tar.gz", hash = "sha256:d27b54c7e170f12ad252fe186e6cab8f3cade8ee4e596682acc5a3afddadb753", size = 49594, upload-time = "2025-04-11T03:52:32.88Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/14/bb49cfba5020c99ec37efcf64bb79f9793c1068992feffa81bcad9e10c92/onnx2torch_py313-1.6.0-py3-none-any.whl", hash = "sha256:03b1cc771f93941cc221635547e97dd7823dffc05f052057e51c6a45fbacba70", size = 81251, upload-time = "2025-04-11T03:52:31.179Z" }, +] + +[[package]] +name = "onnxruntime" +version = "1.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flatbuffers" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "protobuf" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/e4/5353d7e09ced4a8f473f843223fc75d726b2b5519dcefc12f22a6c92852d/onnxruntime-1.27.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:8ba14a38c570087f3cdb8cfba33f7a38a1e826c1e5b29e17c28ceda0cc910016", size = 18416484, upload-time = "2026-06-15T22:43:43.894Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1f/a2117aa3f144fce88774efa37440d0ca72d0c9144854dfc0961f2b04c6fc/onnxruntime-1.27.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2eb083321af8a236a84c7c140a7f4cecbfa2a987a18c07c78db471c20cd390ef", size = 16419330, upload-time = "2026-06-15T22:42:37.58Z" }, + { url = "https://files.pythonhosted.org/packages/e0/cd/74bb804170ceb622fda9111df31a07b3024f7491472256d3a90b5391a4d2/onnxruntime-1.27.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e4f7b0e90d2d212e2c2deaa6c8291616183ab815d3ec558ea12d3ac8b26d36f4", size = 18636930, upload-time = "2026-06-15T22:43:01.584Z" }, + { url = "https://files.pythonhosted.org/packages/fe/8f/5b8e2b85e81735696887175dbaf6409f215683f5ca9d4928fbb038211d32/onnxruntime-1.27.0-cp311-cp311-win_amd64.whl", hash = "sha256:ff050e4f6bf7f12918fa14dcb047c0b02e295f35e86d42532552be4b3d54e977", size = 13356110, upload-time = "2026-06-15T22:43:32.172Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3a/4f568de678126b6a371a93862f015a82138359decd97fcac61fc84b5b774/onnxruntime-1.27.0-cp311-cp311-win_arm64.whl", hash = "sha256:75fbc1e1fb43a39a856c8209c544cca7817b5de7ac16b15b1bdf55d1cc67b9df", size = 13098635, upload-time = "2026-06-15T22:43:19.607Z" }, + { url = "https://files.pythonhosted.org/packages/c3/b7/dd3a524ed93a820dff1af902d0412957ab12499953333e9daa01af5bc480/onnxruntime-1.27.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a14c2ce45312def86b77aea651f46565e45960cf5f0721bfdff449165086ab76", size = 18433506, upload-time = "2026-06-15T22:43:47.026Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/c3b6b17745a1997d784dadc9bd88d713d2e6721139a5a0e885b28cfb79b1/onnxruntime-1.27.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6fddce0539a4898c7bef35b052ffd37935b2190e35488eab99ce91887743ea1", size = 16438140, upload-time = "2026-06-15T22:42:40.666Z" }, + { url = "https://files.pythonhosted.org/packages/26/81/24dd9b31b0fb912ee19ca53ac1c9764bfd79d58a2ccef564eb693be831a5/onnxruntime-1.27.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c65a7438632d55dfbc8a02ee60bd6cf7dd9d1ba05a43d4b851452f32338e194", size = 18658316, upload-time = "2026-06-15T22:43:04.012Z" }, + { url = "https://files.pythonhosted.org/packages/4f/88/8ec9db1a4d126bb8b758992beb40d1249df171917d75f44a327eb5f20dda/onnxruntime-1.27.0-cp312-cp312-win_amd64.whl", hash = "sha256:20c321cf187ba496e648acf6b4cf90b4d398b0d17c2a77fdaeba365b908cc1c1", size = 13358769, upload-time = "2026-06-15T22:43:34.581Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9f/fdad359dfcba7e7cd8815569b304a596531d4efa77a75d77f8b4981891a2/onnxruntime-1.27.0-cp312-cp312-win_arm64.whl", hash = "sha256:d0d1f68868e2ef30ef70998ba9bbbc5c305e9b17041e3936751c1b8aa6aade06", size = 13104440, upload-time = "2026-06-15T22:43:22.893Z" }, +] + +[[package]] +name = "onnxruntime-gpu" +version = "1.23.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coloredlogs" }, + { name = "flatbuffers" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "protobuf" }, + { name = "sympy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/a4/e3d7fbe32b44e814ae24ed642f05fac5d96d120efd82db7a7cac936e85a9/onnxruntime_gpu-1.23.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d76d1ac7a479ecc3ac54482eea4ba3b10d68e888a0f8b5f420f0bdf82c5eec59", size = 300525715, upload-time = "2025-10-22T16:56:19.928Z" }, + { url = "https://files.pythonhosted.org/packages/a9/5c/dba7c009e73dcce02e7f714574345b5e607c5c75510eb8d7bef682b45e5d/onnxruntime_gpu-1.23.2-cp311-cp311-win_amd64.whl", hash = "sha256:054282614c2fc9a4a27d74242afbae706a410f1f63cc35bc72f99709029a5ba4", size = 244506823, upload-time = "2025-10-22T16:55:09.526Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d9/b7140a4f1615195938c7e358c0804bb84271f0d6886b5cbf105c6cb58aae/onnxruntime_gpu-1.23.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f2d1f720685d729b5258ec1b36dee1de381b8898189908c98cbeecdb2f2b5c2", size = 300509596, upload-time = "2025-10-22T16:56:31.728Z" }, + { url = "https://files.pythonhosted.org/packages/87/da/2685c79e5ea587beddebe083601fead0bdf3620bc2f92d18756e7de8a636/onnxruntime_gpu-1.23.2-cp312-cp312-win_amd64.whl", hash = "sha256:fe925a84b00e291e0ad3fac29bfd8f8e06112abc760cdc82cb711b4f3935bd95", size = 244508327, upload-time = "2025-10-22T16:55:19.397Z" }, +] + +[[package]] +name = "openai" +version = "2.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/13/17e87641b89b74552ed408a92b231283786523edddc95f3545809fab673c/openai-2.24.0.tar.gz", hash = "sha256:1e5769f540dbd01cb33bc4716a23e67b9d695161a734aff9c5f925e2bf99a673", size = 658717, upload-time = "2026-02-24T20:02:07.958Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/30/844dc675ee6902579b8eef01ed23917cc9319a1c9c0c14ec6e39340c96d0/openai-2.24.0-py3-none-any.whl", hash = "sha256:fed30480d7d6c884303287bde864980a4b137b60553ffbcf9ab4a233b7a73d94", size = 1120122, upload-time = "2026-02-24T20:02:05.669Z" }, +] + +[[package]] +name = "optuna" +version = "4.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alembic" }, + { name = "colorlog" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "sqlalchemy" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/b2/b5e12de7b4486556fe2257611b55dbabf30d0300bdb031831aa943ad20e4/optuna-4.7.0.tar.gz", hash = "sha256:d91817e2079825557bd2e97de2e8c9ae260bfc99b32712502aef8a5095b2d2c0", size = 479740, upload-time = "2026-01-19T05:45:52.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/d1/6c8a4fbb38a9e3565f5c36b871262a85ecab3da48120af036b1e4937a15c/optuna-4.7.0-py3-none-any.whl", hash = "sha256:e41ec84018cecc10eabf28143573b1f0bde0ba56dba8151631a590ecbebc1186", size = 413894, upload-time = "2026-01-19T05:45:50.815Z" }, +] + +[[package]] +name = "orjson" +version = "3.11.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/a3/4e09c61a5f0c521cba0bb433639610ae037437669f1a4cbc93799e731d78/orjson-3.11.6.tar.gz", hash = "sha256:0a54c72259f35299fd033042367df781c2f66d10252955ca1efb7db309b954cb", size = 6175856, upload-time = "2026-01-29T15:13:07.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/fd/d6b0a36854179b93ed77839f107c4089d91cccc9f9ba1b752b6e3bac5f34/orjson-3.11.6-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e259e85a81d76d9665f03d6129e09e4435531870de5961ddcd0bf6e3a7fde7d7", size = 250029, upload-time = "2026-01-29T15:11:35.942Z" }, + { url = "https://files.pythonhosted.org/packages/a3/bb/22902619826641cf3b627c24aab62e2ad6b571bdd1d34733abb0dd57f67a/orjson-3.11.6-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:52263949f41b4a4822c6b1353bcc5ee2f7109d53a3b493501d3369d6d0e7937a", size = 134518, upload-time = "2026-01-29T15:11:37.347Z" }, + { url = "https://files.pythonhosted.org/packages/72/90/7a818da4bba1de711a9653c420749c0ac95ef8f8651cbc1dca551f462fe0/orjson-3.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6439e742fa7834a24698d358a27346bb203bff356ae0402e7f5df8f749c621a8", size = 137917, upload-time = "2026-01-29T15:11:38.511Z" }, + { url = "https://files.pythonhosted.org/packages/59/0f/02846c1cac8e205cb3822dd8aa8f9114acda216f41fd1999ace6b543418d/orjson-3.11.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b81ffd68f084b4e993e3867acb554a049fa7787cc8710bbcc1e26965580d99be", size = 134923, upload-time = "2026-01-29T15:11:39.711Z" }, + { url = "https://files.pythonhosted.org/packages/94/cf/aeaf683001b474bb3c3c757073a4231dfdfe8467fceaefa5bfd40902c99f/orjson-3.11.6-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5a5468e5e60f7ef6d7f9044b06c8f94a3c56ba528c6e4f7f06ae95164b595ec", size = 140752, upload-time = "2026-01-29T15:11:41.347Z" }, + { url = "https://files.pythonhosted.org/packages/fc/fe/dad52d8315a65f084044a0819d74c4c9daf9ebe0681d30f525b0d29a31f0/orjson-3.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:72c5005eb45bd2535632d4f3bec7ad392832cfc46b62a3021da3b48a67734b45", size = 144201, upload-time = "2026-01-29T15:11:42.537Z" }, + { url = "https://files.pythonhosted.org/packages/36/bc/ab070dd421565b831801077f1e390c4d4af8bfcecafc110336680a33866b/orjson-3.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0b14dd49f3462b014455a28a4d810d3549bf990567653eb43765cd847df09145", size = 142380, upload-time = "2026-01-29T15:11:44.309Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d8/4b581c725c3a308717f28bf45a9fdac210bca08b67e8430143699413ff06/orjson-3.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e0bb2c1ea30ef302f0f89f9bf3e7f9ab5e2af29dc9f80eb87aa99788e4e2d65", size = 145582, upload-time = "2026-01-29T15:11:45.506Z" }, + { url = "https://files.pythonhosted.org/packages/5b/a2/09aab99b39f9a7f175ea8fa29adb9933a3d01e7d5d603cdee7f1c40c8da2/orjson-3.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:825e0a85d189533c6bff7e2fc417a28f6fcea53d27125c4551979aecd6c9a197", size = 147270, upload-time = "2026-01-29T15:11:46.782Z" }, + { url = "https://files.pythonhosted.org/packages/b8/2f/5ef8eaf7829dc50da3bf497c7775b21ee88437bc8c41f959aa3504ca6631/orjson-3.11.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:b04575417a26530637f6ab4b1f7b4f666eb0433491091da4de38611f97f2fcf3", size = 421222, upload-time = "2026-01-29T15:11:48.106Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b0/dd6b941294c2b5b13da5fdc7e749e58d0c55a5114ab37497155e83050e95/orjson-3.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b83eb2e40e8c4da6d6b340ee6b1d6125f5195eb1b0ebb7eac23c6d9d4f92d224", size = 155562, upload-time = "2026-01-29T15:11:49.408Z" }, + { url = "https://files.pythonhosted.org/packages/8e/09/43924331a847476ae2f9a16bd6d3c9dab301265006212ba0d3d7fd58763a/orjson-3.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1f42da604ee65a6b87eef858c913ce3e5777872b19321d11e6fc6d21de89b64f", size = 147432, upload-time = "2026-01-29T15:11:50.635Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e9/d9865961081816909f6b49d880749dbbd88425afd7c5bbce0549e2290d77/orjson-3.11.6-cp311-cp311-win32.whl", hash = "sha256:5ae45df804f2d344cffb36c43fdf03c82fb6cd247f5faa41e21891b40dfbf733", size = 139623, upload-time = "2026-01-29T15:11:51.82Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f9/6836edb92f76eec1082919101eb1145d2f9c33c8f2c5e6fa399b82a2aaa8/orjson-3.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:f4295948d65ace0a2d8f2c4ccc429668b7eb8af547578ec882e16bf79b0050b2", size = 136647, upload-time = "2026-01-29T15:11:53.454Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0c/4954082eea948c9ae52ee0bcbaa2f99da3216a71bcc314ab129bde22e565/orjson-3.11.6-cp311-cp311-win_arm64.whl", hash = "sha256:314e9c45e0b81b547e3a1cfa3df3e07a815821b3dac9fe8cb75014071d0c16a4", size = 135327, upload-time = "2026-01-29T15:11:56.616Z" }, + { url = "https://files.pythonhosted.org/packages/14/ba/759f2879f41910b7e5e0cdbd9cf82a4f017c527fb0e972e9869ca7fe4c8e/orjson-3.11.6-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6f03f30cd8953f75f2a439070c743c7336d10ee940da918d71c6f3556af3ddcf", size = 249988, upload-time = "2026-01-29T15:11:58.294Z" }, + { url = "https://files.pythonhosted.org/packages/f0/70/54cecb929e6c8b10104fcf580b0cc7dc551aa193e83787dd6f3daba28bb5/orjson-3.11.6-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:af44baae65ef386ad971469a8557a0673bb042b0b9fd4397becd9c2dfaa02588", size = 134445, upload-time = "2026-01-29T15:11:59.819Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6f/ec0309154457b9ba1ad05f11faa4441f76037152f75e1ac577db3ce7ca96/orjson-3.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c310a48542094e4f7dbb6ac076880994986dda8ca9186a58c3cb70a3514d3231", size = 137708, upload-time = "2026-01-29T15:12:01.488Z" }, + { url = "https://files.pythonhosted.org/packages/20/52/3c71b80840f8bab9cb26417302707b7716b7d25f863f3a541bcfa232fe6e/orjson-3.11.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d8dfa7a5d387f15ecad94cb6b2d2d5f4aeea64efd8d526bfc03c9812d01e1cc0", size = 134798, upload-time = "2026-01-29T15:12:02.705Z" }, + { url = "https://files.pythonhosted.org/packages/30/51/b490a43b22ff736282360bd02e6bded455cf31dfc3224e01cd39f919bbd2/orjson-3.11.6-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba8daee3e999411b50f8b50dbb0a3071dd1845f3f9a1a0a6fa6de86d1689d84d", size = 140839, upload-time = "2026-01-29T15:12:03.956Z" }, + { url = "https://files.pythonhosted.org/packages/95/bc/4bcfe4280c1bc63c5291bb96f98298845b6355da2226d3400e17e7b51e53/orjson-3.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f89d104c974eafd7436d7a5fdbc57f7a1e776789959a2f4f1b2eab5c62a339f4", size = 144080, upload-time = "2026-01-29T15:12:05.151Z" }, + { url = "https://files.pythonhosted.org/packages/01/74/22970f9ead9ab1f1b5f8c227a6c3aa8d71cd2c5acd005868a1d44f2362fa/orjson-3.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2e2e2456788ca5ea75616c40da06fc885a7dc0389780e8a41bf7c5389ba257b", size = 142435, upload-time = "2026-01-29T15:12:06.641Z" }, + { url = "https://files.pythonhosted.org/packages/29/34/d564aff85847ab92c82ee43a7a203683566c2fca0723a5f50aebbe759603/orjson-3.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a42efebc45afabb1448001e90458c4020d5c64fbac8a8dc4045b777db76cb5a", size = 145631, upload-time = "2026-01-29T15:12:08.351Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ef/016957a3890752c4aa2368326ea69fa53cdc1fdae0a94a542b6410dbdf52/orjson-3.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:71b7cbef8471324966c3738c90ba38775563ef01b512feb5ad4805682188d1b9", size = 147058, upload-time = "2026-01-29T15:12:10.023Z" }, + { url = "https://files.pythonhosted.org/packages/56/cc/9a899c3972085645b3225569f91a30e221f441e5dc8126e6d060b971c252/orjson-3.11.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f8515e5910f454fe9a8e13c2bb9dc4bae4c1836313e967e72eb8a4ad874f0248", size = 421161, upload-time = "2026-01-29T15:12:11.308Z" }, + { url = "https://files.pythonhosted.org/packages/21/a8/767d3fbd6d9b8fdee76974db40619399355fd49bf91a6dd2c4b6909ccf05/orjson-3.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:300360edf27c8c9bf7047345a94fddf3a8b8922df0ff69d71d854a170cb375cf", size = 155757, upload-time = "2026-01-29T15:12:12.776Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0b/205cd69ac87e2272e13ef3f5f03a3d4657e317e38c1b08aaa2ef97060bbc/orjson-3.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:caaed4dad39e271adfadc106fab634d173b2bb23d9cf7e67bd645f879175ebfc", size = 147446, upload-time = "2026-01-29T15:12:14.166Z" }, + { url = "https://files.pythonhosted.org/packages/de/c5/dd9f22aa9f27c54c7d05cc32f4580c9ac9b6f13811eeb81d6c4c3f50d6b1/orjson-3.11.6-cp312-cp312-win32.whl", hash = "sha256:955368c11808c89793e847830e1b1007503a5923ddadc108547d3b77df761044", size = 139717, upload-time = "2026-01-29T15:12:15.7Z" }, + { url = "https://files.pythonhosted.org/packages/23/a1/e62fc50d904486970315a1654b8cfb5832eb46abb18cd5405118e7e1fc79/orjson-3.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:2c68de30131481150073d90a5d227a4a421982f42c025ecdfb66157f9579e06f", size = 136711, upload-time = "2026-01-29T15:12:17.055Z" }, + { url = "https://files.pythonhosted.org/packages/04/3d/b4fefad8bdf91e0fe212eb04975aeb36ea92997269d68857efcc7eb1dda3/orjson-3.11.6-cp312-cp312-win_arm64.whl", hash = "sha256:65dfa096f4e3a5e02834b681f539a87fbe85adc82001383c0db907557f666bfc", size = 135212, upload-time = "2026-01-29T15:12:18.3Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pandas" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213, upload-time = "2024-09-20T13:10:04.827Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/44/d9502bf0ed197ba9bf1103c9867d5904ddcaf869e52329787fc54ed70cc8/pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039", size = 12602222, upload-time = "2024-09-20T13:08:56.254Z" }, + { url = "https://files.pythonhosted.org/packages/52/11/9eac327a38834f162b8250aab32a6781339c69afe7574368fffe46387edf/pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd", size = 11321274, upload-time = "2024-09-20T13:08:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/45/fb/c4beeb084718598ba19aa9f5abbc8aed8b42f90930da861fcb1acdb54c3a/pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698", size = 15579836, upload-time = "2024-09-20T19:01:57.571Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5f/4dba1d39bb9c38d574a9a22548c540177f78ea47b32f99c0ff2ec499fac5/pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc", size = 13058505, upload-time = "2024-09-20T13:09:01.501Z" }, + { url = "https://files.pythonhosted.org/packages/b9/57/708135b90391995361636634df1f1130d03ba456e95bcf576fada459115a/pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3", size = 16744420, upload-time = "2024-09-20T19:02:00.678Z" }, + { url = "https://files.pythonhosted.org/packages/86/4a/03ed6b7ee323cf30404265c284cee9c65c56a212e0a08d9ee06984ba2240/pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32", size = 14440457, upload-time = "2024-09-20T13:09:04.105Z" }, + { url = "https://files.pythonhosted.org/packages/ed/8c/87ddf1fcb55d11f9f847e3c69bb1c6f8e46e2f40ab1a2d2abadb2401b007/pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5", size = 11617166, upload-time = "2024-09-20T13:09:06.917Z" }, + { url = "https://files.pythonhosted.org/packages/17/a3/fb2734118db0af37ea7433f57f722c0a56687e14b14690edff0cdb4b7e58/pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9", size = 12529893, upload-time = "2024-09-20T13:09:09.655Z" }, + { url = "https://files.pythonhosted.org/packages/e1/0c/ad295fd74bfac85358fd579e271cded3ac969de81f62dd0142c426b9da91/pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4", size = 11363475, upload-time = "2024-09-20T13:09:14.718Z" }, + { url = "https://files.pythonhosted.org/packages/c6/2a/4bba3f03f7d07207481fed47f5b35f556c7441acddc368ec43d6643c5777/pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3", size = 15188645, upload-time = "2024-09-20T19:02:03.88Z" }, + { url = "https://files.pythonhosted.org/packages/38/f8/d8fddee9ed0d0c0f4a2132c1dfcf0e3e53265055da8df952a53e7eaf178c/pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319", size = 12739445, upload-time = "2024-09-20T13:09:17.621Z" }, + { url = "https://files.pythonhosted.org/packages/20/e8/45a05d9c39d2cea61ab175dbe6a2de1d05b679e8de2011da4ee190d7e748/pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8", size = 16359235, upload-time = "2024-09-20T19:02:07.094Z" }, + { url = "https://files.pythonhosted.org/packages/1d/99/617d07a6a5e429ff90c90da64d428516605a1ec7d7bea494235e1c3882de/pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a", size = 14056756, upload-time = "2024-09-20T13:09:20.474Z" }, + { url = "https://files.pythonhosted.org/packages/29/d4/1244ab8edf173a10fd601f7e13b9566c1b525c4f365d6bee918e68381889/pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", size = 11504248, upload-time = "2024-09-20T13:09:23.137Z" }, +] + +[[package]] +name = "pillow" +version = "12.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/02/d52c733a2452ef1ffcc123b68e6606d07276b0e358db70eabad7e40042b7/pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9", size = 46977283, upload-time = "2026-01-02T09:13:29.892Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/c4/bf8328039de6cc22182c3ef007a2abfbbdab153661c0a9aa78af8d706391/pillow-12.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:a83e0850cb8f5ac975291ebfc4170ba481f41a28065277f7f735c202cd8e0af3", size = 5304057, upload-time = "2026-01-02T09:10:46.627Z" }, + { url = "https://files.pythonhosted.org/packages/43/06/7264c0597e676104cc22ca73ee48f752767cd4b1fe084662620b17e10120/pillow-12.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b6e53e82ec2db0717eabb276aa56cf4e500c9a7cec2c2e189b55c24f65a3e8c0", size = 4657811, upload-time = "2026-01-02T09:10:49.548Z" }, + { url = "https://files.pythonhosted.org/packages/72/64/f9189e44474610daf83da31145fa56710b627b5c4c0b9c235e34058f6b31/pillow-12.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:40a8e3b9e8773876d6e30daed22f016509e3987bab61b3b7fe309d7019a87451", size = 6232243, upload-time = "2026-01-02T09:10:51.62Z" }, + { url = "https://files.pythonhosted.org/packages/ef/30/0df458009be6a4caca4ca2c52975e6275c387d4e5c95544e34138b41dc86/pillow-12.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:800429ac32c9b72909c671aaf17ecd13110f823ddb7db4dfef412a5587c2c24e", size = 8037872, upload-time = "2026-01-02T09:10:53.446Z" }, + { url = "https://files.pythonhosted.org/packages/e4/86/95845d4eda4f4f9557e25381d70876aa213560243ac1a6d619c46caaedd9/pillow-12.1.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b022eaaf709541b391ee069f0022ee5b36c709df71986e3f7be312e46f42c84", size = 6345398, upload-time = "2026-01-02T09:10:55.426Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1f/8e66ab9be3aaf1435bc03edd1ebdf58ffcd17f7349c1d970cafe87af27d9/pillow-12.1.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f345e7bc9d7f368887c712aa5054558bad44d2a301ddf9248599f4161abc7c0", size = 7034667, upload-time = "2026-01-02T09:10:57.11Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f6/683b83cb9b1db1fb52b87951b1c0b99bdcfceaa75febf11406c19f82cb5e/pillow-12.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d70347c8a5b7ccd803ec0c85c8709f036e6348f1e6a5bf048ecd9c64d3550b8b", size = 6458743, upload-time = "2026-01-02T09:10:59.331Z" }, + { url = "https://files.pythonhosted.org/packages/9a/7d/de833d63622538c1d58ce5395e7c6cb7e7dce80decdd8bde4a484e095d9f/pillow-12.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1fcc52d86ce7a34fd17cb04e87cfdb164648a3662a6f20565910a99653d66c18", size = 7159342, upload-time = "2026-01-02T09:11:01.82Z" }, + { url = "https://files.pythonhosted.org/packages/8c/40/50d86571c9e5868c42b81fe7da0c76ca26373f3b95a8dd675425f4a92ec1/pillow-12.1.0-cp311-cp311-win32.whl", hash = "sha256:3ffaa2f0659e2f740473bcf03c702c39a8d4b2b7ffc629052028764324842c64", size = 6328655, upload-time = "2026-01-02T09:11:04.556Z" }, + { url = "https://files.pythonhosted.org/packages/6c/af/b1d7e301c4cd26cd45d4af884d9ee9b6fab893b0ad2450d4746d74a6968c/pillow-12.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:806f3987ffe10e867bab0ddad45df1148a2b98221798457fa097ad85d6e8bc75", size = 7031469, upload-time = "2026-01-02T09:11:06.538Z" }, + { url = "https://files.pythonhosted.org/packages/48/36/d5716586d887fb2a810a4a61518a327a1e21c8b7134c89283af272efe84b/pillow-12.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:9f5fefaca968e700ad1a4a9de98bf0869a94e397fe3524c4c9450c1445252304", size = 2452515, upload-time = "2026-01-02T09:11:08.226Z" }, + { url = "https://files.pythonhosted.org/packages/20/31/dc53fe21a2f2996e1b7d92bf671cdb157079385183ef7c1ae08b485db510/pillow-12.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b", size = 5262642, upload-time = "2026-01-02T09:11:10.138Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c1/10e45ac9cc79419cedf5121b42dcca5a50ad2b601fa080f58c22fb27626e/pillow-12.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551", size = 4657464, upload-time = "2026-01-02T09:11:12.319Z" }, + { url = "https://files.pythonhosted.org/packages/ad/26/7b82c0ab7ef40ebede7a97c72d473bda5950f609f8e0c77b04af574a0ddb/pillow-12.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208", size = 6234878, upload-time = "2026-01-02T09:11:14.096Z" }, + { url = "https://files.pythonhosted.org/packages/76/25/27abc9792615b5e886ca9411ba6637b675f1b77af3104710ac7353fe5605/pillow-12.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5", size = 8044868, upload-time = "2026-01-02T09:11:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ea/f200a4c36d836100e7bc738fc48cd963d3ba6372ebc8298a889e0cfc3359/pillow-12.1.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661", size = 6349468, upload-time = "2026-01-02T09:11:17.631Z" }, + { url = "https://files.pythonhosted.org/packages/11/8f/48d0b77ab2200374c66d344459b8958c86693be99526450e7aee714e03e4/pillow-12.1.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17", size = 7041518, upload-time = "2026-01-02T09:11:19.389Z" }, + { url = "https://files.pythonhosted.org/packages/1d/23/c281182eb986b5d31f0a76d2a2c8cd41722d6fb8ed07521e802f9bba52de/pillow-12.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670", size = 6462829, upload-time = "2026-01-02T09:11:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/25/ef/7018273e0faac099d7b00982abdcc39142ae6f3bd9ceb06de09779c4a9d6/pillow-12.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616", size = 7166756, upload-time = "2026-01-02T09:11:23.559Z" }, + { url = "https://files.pythonhosted.org/packages/8f/c8/993d4b7ab2e341fe02ceef9576afcf5830cdec640be2ac5bee1820d693d4/pillow-12.1.0-cp312-cp312-win32.whl", hash = "sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7", size = 6328770, upload-time = "2026-01-02T09:11:25.661Z" }, + { url = "https://files.pythonhosted.org/packages/a7/87/90b358775a3f02765d87655237229ba64a997b87efa8ccaca7dd3e36e7a7/pillow-12.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d", size = 7033406, upload-time = "2026-01-02T09:11:27.474Z" }, + { url = "https://files.pythonhosted.org/packages/5d/cf/881b457eccacac9e5b2ddd97d5071fb6d668307c57cbf4e3b5278e06e536/pillow-12.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c", size = 2452612, upload-time = "2026-01-02T09:11:29.309Z" }, + { url = "https://files.pythonhosted.org/packages/8b/bc/224b1d98cffd7164b14707c91aac83c07b047fbd8f58eba4066a3e53746a/pillow-12.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ca94b6aac0d7af2a10ba08c0f888b3d5114439b6b3ef39968378723622fed377", size = 5228605, upload-time = "2026-01-02T09:13:14.084Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ca/49ca7769c4550107de049ed85208240ba0f330b3f2e316f24534795702ce/pillow-12.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:351889afef0f485b84078ea40fe33727a0492b9af3904661b0abbafee0355b72", size = 4622245, upload-time = "2026-01-02T09:13:15.964Z" }, + { url = "https://files.pythonhosted.org/packages/73/48/fac807ce82e5955bcc2718642b94b1bd22a82a6d452aea31cbb678cddf12/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb0984b30e973f7e2884362b7d23d0a348c7143ee559f38ef3eaab640144204c", size = 5247593, upload-time = "2026-01-02T09:13:17.913Z" }, + { url = "https://files.pythonhosted.org/packages/d2/95/3e0742fe358c4664aed4fd05d5f5373dcdad0b27af52aa0972568541e3f4/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:84cabc7095dd535ca934d57e9ce2a72ffd216e435a84acb06b2277b1de2689bd", size = 6989008, upload-time = "2026-01-02T09:13:20.083Z" }, + { url = "https://files.pythonhosted.org/packages/5a/74/fe2ac378e4e202e56d50540d92e1ef4ff34ed687f3c60f6a121bcf99437e/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53d8b764726d3af1a138dd353116f774e3862ec7e3794e0c8781e30db0f35dfc", size = 5313824, upload-time = "2026-01-02T09:13:22.405Z" }, + { url = "https://files.pythonhosted.org/packages/f3/77/2a60dee1adee4e2655ac328dd05c02a955c1cd683b9f1b82ec3feb44727c/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5da841d81b1a05ef940a8567da92decaa15bc4d7dedb540a8c219ad83d91808a", size = 5963278, upload-time = "2026-01-02T09:13:24.706Z" }, + { url = "https://files.pythonhosted.org/packages/2d/71/64e9b1c7f04ae0027f788a248e6297d7fcc29571371fe7d45495a78172c0/pillow-12.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:75af0b4c229ac519b155028fa1be632d812a519abba9b46b20e50c6caa184f19", size = 7029809, upload-time = "2026-01-02T09:13:26.541Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, +] + +[[package]] +name = "playwright" +version = "1.60.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet" }, + { name = "pyee" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/f0/832bd9677194908da118064eef20082f2791e3d18215cc6d9391ee2c5a67/playwright-1.60.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:6a8cd0fec171fb3089e95e898c8bc8a6f35dea0b78b399e12fcc19427e91b1d7", size = 43474635, upload-time = "2026-05-18T12:00:31.969Z" }, + { url = "https://files.pythonhosted.org/packages/59/7b/e1d32ae8a3ed937ec2be3721c5f728b13d731a0b7c6442e0b3bec5094ac0/playwright-1.60.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:39b5420ba6145045b69ced4c5c47d4d9fe5bddfc8ff816c518913afcb25ec7a5", size = 42261327, upload-time = "2026-05-18T12:00:35.638Z" }, + { url = "https://files.pythonhosted.org/packages/d7/bc/23de499ded6411c188a20c5a0dea6f0cd4ed5d2b3cc6042a5dbd3ed609aa/playwright-1.60.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:2581d0e6a3392c71f91b27460c7fd093356818dc430f48153896c8aeeaef7705", size = 43474636, upload-time = "2026-05-18T12:00:39.294Z" }, + { url = "https://files.pythonhosted.org/packages/22/7b/1d679f4fced4ea94efadd17103856d8c565384f68382a1681264e46f5925/playwright-1.60.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:1c2bfae7884fb3fb05b853290eab8f343d524e5016f2f1def702acbbdf14c93e", size = 47467220, upload-time = "2026-05-18T12:00:43.179Z" }, + { url = "https://files.pythonhosted.org/packages/84/c2/1528d267d4442bd2c6b8eaeab819dd52c2030bf80e89293f0ba1f687473b/playwright-1.60.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43e66564125ee31b07a58cefb21e256d62d67d8d1713e6858df7a3019d8ed353", size = 47154856, upload-time = "2026-05-18T12:00:46.715Z" }, + { url = "https://files.pythonhosted.org/packages/bb/4e/b008b6440a7a1624378041da94829956d4b8f7ab9ef5aad22d0dc3f2e26d/playwright-1.60.0-py3-none-win32.whl", hash = "sha256:ec94e416ea320711e0ad4bf185dcbf41833672961e90773e1885255d7db7b7e7", size = 37902157, upload-time = "2026-05-18T12:00:50.374Z" }, + { url = "https://files.pythonhosted.org/packages/55/f0/0541524133104f9cc20bf900870ff4a736b76a23483f3a55295ddfa58409/playwright-1.60.0-py3-none-win_amd64.whl", hash = "sha256:9566821ce6030a1f9e7146a24e19355ab0d98805fd0f9be50bb3d8fef1750c02", size = 37902159, upload-time = "2026-05-18T12:00:53.728Z" }, + { url = "https://files.pythonhosted.org/packages/80/c8/210f282d278e4709cdd71b12a31af45a30a22ab3207b387e29b37e478713/playwright-1.60.0-py3-none-win_arm64.whl", hash = "sha256:6e4f6700a4c2250efff8e690a81d66e3855754fb587b6b87cf5c784014f91537", size = 34037981, upload-time = "2026-05-18T12:00:57.584Z" }, +] + +[[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.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "platformdirs" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/77/b3d3e00c696c16cf99af81ef7b1f5fe73bd2a307abca41bd7605429fe6e5/pooch-1.8.2.tar.gz", hash = "sha256:76561f0de68a01da4df6af38e9955c4c9d1a5c90da73f7e40276a5728ec83d10", size = 59353, upload-time = "2024-06-06T16:53:46.224Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/87/77cc11c7a9ea9fd05503def69e3d18605852cd0d4b0d3b8f15bbeb3ef1d1/pooch-1.8.2-py3-none-any.whl", hash = "sha256:3529a57096f7198778a5ceefd5ac3ef0e4d06a6ddaf9fc2d609b806f25302c47", size = 64574, upload-time = "2024-06-06T16:53:44.343Z" }, +] + +[[package]] +name = "primepy" +version = "1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/77/0cfa1b4697cfb5336f3a96e8bc73327f64610be3a64c97275f1801afb395/primePy-1.3.tar.gz", hash = "sha256:25fd7e25344b0789a5984c75d89f054fcf1f180bef20c998e4befbac92de4669", size = 3914, upload-time = "2018-05-29T17:18:18.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/c1/bb7e334135859c3a92ec399bc89293ea73f28e815e35b43929c8db6af030/primePy-1.3-py3-none-any.whl", hash = "sha256:5ed443718765be9bf7e2ff4c56cdff71b42140a15b39d054f9d99f0009e2317a", size = 4040, upload-time = "2018-05-29T17:18:17.53Z" }, +] + +[[package]] +name = "propcache" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, + { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" }, + { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" }, + { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" }, + { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" }, + { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, + { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, + { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/b8/cda15d9d46d03d4aa3a67cb6bffe05173440ccf86a9541afaf7ac59a1b6b/protobuf-6.33.4.tar.gz", hash = "sha256:dc2e61bca3b10470c1912d166fe0af67bfc20eb55971dcef8dfa48ce14f0ed91", size = 444346, upload-time = "2026-01-12T18:33:40.109Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/be/24ef9f3095bacdf95b458543334d0c4908ccdaee5130420bf064492c325f/protobuf-6.33.4-cp310-abi3-win32.whl", hash = "sha256:918966612c8232fc6c24c78e1cd89784307f5814ad7506c308ee3cf86662850d", size = 425612, upload-time = "2026-01-12T18:33:29.656Z" }, + { url = "https://files.pythonhosted.org/packages/31/ad/e5693e1974a28869e7cd244302911955c1cebc0161eb32dfa2b25b6e96f0/protobuf-6.33.4-cp310-abi3-win_amd64.whl", hash = "sha256:8f11ffae31ec67fc2554c2ef891dcb561dae9a2a3ed941f9e134c2db06657dbc", size = 436962, upload-time = "2026-01-12T18:33:31.345Z" }, + { url = "https://files.pythonhosted.org/packages/66/15/6ee23553b6bfd82670207ead921f4d8ef14c107e5e11443b04caeb5ab5ec/protobuf-6.33.4-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2fe67f6c014c84f655ee06f6f66213f9254b3a8b6bda6cda0ccd4232c73c06f0", size = 427612, upload-time = "2026-01-12T18:33:32.646Z" }, + { url = "https://files.pythonhosted.org/packages/2b/48/d301907ce6d0db75f959ca74f44b475a9caa8fcba102d098d3c3dd0f2d3f/protobuf-6.33.4-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:757c978f82e74d75cba88eddec479df9b99a42b31193313b75e492c06a51764e", size = 324484, upload-time = "2026-01-12T18:33:33.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/1c/e53078d3f7fe710572ab2dcffd993e1e3b438ae71cfc031b71bae44fcb2d/protobuf-6.33.4-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c7c64f259c618f0bef7bee042075e390debbf9682334be2b67408ec7c1c09ee6", size = 339256, upload-time = "2026-01-12T18:33:35.231Z" }, + { url = "https://files.pythonhosted.org/packages/e8/8e/971c0edd084914f7ee7c23aa70ba89e8903918adca179319ee94403701d5/protobuf-6.33.4-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:3df850c2f8db9934de4cf8f9152f8dc2558f49f298f37f90c517e8e5c84c30e9", size = 323311, upload-time = "2026-01-12T18:33:36.305Z" }, + { url = "https://files.pythonhosted.org/packages/75/b1/1dc83c2c661b4c62d56cc081706ee33a4fc2835bd90f965baa2663ef7676/protobuf-6.33.4-py3-none-any.whl", hash = "sha256:1fe3730068fcf2e595816a6c34fe66eeedd37d51d0400b72fabc848811fdc1bc", size = 170532, upload-time = "2026-01-12T18:33:39.199Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "pyannote-audio" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asteroid-filterbanks" }, + { name = "einops" }, + { name = "huggingface-hub" }, + { name = "lightning" }, + { name = "omegaconf" }, + { name = "pyannote-core" }, + { name = "pyannote-database" }, + { name = "pyannote-metrics" }, + { name = "pyannote-pipeline" }, + { name = "pytorch-metric-learning" }, + { name = "rich" }, + { name = "semver" }, + { name = "soundfile" }, + { name = "speechbrain" }, + { name = "tensorboardx" }, + { name = "torch" }, + { name = "torch-audiomentations" }, + { name = "torchaudio" }, + { name = "torchmetrics" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/1e/efe9619c38f1281ddf21640654d8ea9e3f67c459b76f78657b26d8557bbe/pyannote_audio-3.4.0.tar.gz", hash = "sha256:d523d883cb8d37cb6daf99f3ba83f9138bb193646ad71e6eae7deb89d8ddd642", size = 804850, upload-time = "2025-09-09T07:04:51.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/13/620c6f711b723653092fd063bfee82a6af5ea3a4d3c42efc53ce623a7f4d/pyannote_audio-3.4.0-py2.py3-none-any.whl", hash = "sha256:36e38f058059f46da3478dda581cda53d9d85a21173a3e70bbdbc3ba93b5e1b7", size = 897789, upload-time = "2025-09-09T07:04:49.464Z" }, +] + +[[package]] +name = "pyannote-core" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "scipy" }, + { name = "sortedcontainers" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/03/feaf7534206f02c75baf151ce4b8c322b402a6f477c2be82f69d9269cbe6/pyannote.core-5.0.0.tar.gz", hash = "sha256:1a55bcc8bd680ba6be5fa53efa3b6f3d2cdd67144c07b6b4d8d66d5cb0d2096f", size = 59247, upload-time = "2022-12-15T13:02:05.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/c4/370bc8ba66815a5832ece753a1009388bb07ea353d21c83f2d5a1a436f2c/pyannote.core-5.0.0-py3-none-any.whl", hash = "sha256:04920a6754492242ce0dc6017545595ab643870fe69a994f20c1a5f2da0544d0", size = 58475, upload-time = "2022-12-15T13:02:03.265Z" }, +] + +[[package]] +name = "pyannote-database" +version = "5.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pandas" }, + { name = "pyannote-core" }, + { name = "pyyaml" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/ae/de36413d69a46be87cb612ebbcdc4eacbeebce3bc809124603e44a88fe26/pyannote.database-5.1.3.tar.gz", hash = "sha256:0eaf64c1cc506718de60d2d702f1359b1ae7ff252ee3e4799f1c5e378cd52c31", size = 49957, upload-time = "2025-01-15T20:28:26.437Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/64/92d51a3a05615ba58be8ba62a43f9f9f952d9f3646f7e4fb7826e5a3a24e/pyannote.database-5.1.3-py3-none-any.whl", hash = "sha256:37887844c7dfbcc075cb591eddc00aff45fae1ed905344e1f43e0090e63bd40a", size = 48127, upload-time = "2025-01-15T20:28:25.326Z" }, +] + +[[package]] +name = "pyannote-metrics" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docopt" }, + { name = "matplotlib" }, + { name = "numpy" }, + { name = "pandas" }, + { name = "pyannote-core" }, + { name = "pyannote-database" }, + { name = "scikit-learn" }, + { name = "scipy" }, + { name = "sympy" }, + { name = "tabulate" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/2b/6c5f01d3c49aa1c160765946e23782ca6436ae8b9bc514b56319ff5f16e7/pyannote.metrics-3.2.1.tar.gz", hash = "sha256:08024255a3550e96a8e9da4f5f4af326886548480de891414567c8900920ee5c", size = 49086, upload-time = "2022-06-20T14:10:34.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/7d/035b370ab834b30e849fe9cd092b7bd7f321fcc4a2c56b84e96476b7ede5/pyannote.metrics-3.2.1-py3-none-any.whl", hash = "sha256:46be797cdade26c82773e5018659ae610145260069c7c5bf3d3c8a029ade8e22", size = 51386, upload-time = "2022-06-20T14:10:32.621Z" }, +] + +[[package]] +name = "pyannote-pipeline" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docopt" }, + { name = "filelock" }, + { name = "optuna" }, + { name = "pyannote-core" }, + { name = "pyannote-database" }, + { name = "pyyaml" }, + { name = "scikit-learn" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/35/04/4bcfe0dd588577a188328b806f3a7213d8cead0ce5fe5784d01fd57df93f/pyannote.pipeline-3.0.1.tar.gz", hash = "sha256:021794e26a2cf5d8fb5bb1835951e71f5fac33eb14e23dfb7468e16b1b805151", size = 34486, upload-time = "2023-09-22T20:16:49.951Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/42/1bf7cbf061ed05c580bfb63bffdd3f3474cbd5c02bee4fac518eea9e9d9e/pyannote.pipeline-3.0.1-py3-none-any.whl", hash = "sha256:819bde4c4dd514f740f2373dfec794832b9fc8e346a35e43a7681625ee187393", size = 31517, upload-time = "2023-09-22T20:16:48.153Z" }, +] + +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.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/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, +] + +[[package]] +name = "pydub" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/9a/e6bca0eed82db26562c73b5076539a4a08d3cffd19c3cc5913a3e61145fd/pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f", size = 38326, upload-time = "2021-03-10T02:09:54.659Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/53/d78dc063216e62fc55f6b2eebb447f6a4b0a59f55c8406376f76bf959b08/pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6", size = 32327, upload-time = "2021-03-10T02:09:53.503Z" }, +] + +[[package]] +name = "pyee" +version = "13.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/04/e7c1fe4dc78a6fdbfd6c337b1c3732ff543b8a397683ab38378447baa331/pyee-13.0.1.tar.gz", hash = "sha256:0b931f7c14535667ed4c7e0d531716368715e860b988770fc7eb8578d1f67fc8", size = 31655, upload-time = "2026-02-14T21:12:28.044Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/b4d4827c93ef43c01f599ef31453ccc1c132b353284fc6c87d535c233129/pyee-13.0.1-py3-none-any.whl", hash = "sha256:af2f8fede4171ef667dfded53f96e2ed0d6e6bd7ee3bb46437f77e3b57689228", size = 15659, upload-time = "2026-02-14T21:12:26.263Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/c1/1d9de9aeaa1b89b0186e5fe23294ff6517fce1bc69149185577cd31016b2/pyparsing-3.3.1.tar.gz", hash = "sha256:47fad0f17ac1e2cad3de3b458570fbc9b03560aa029ed5e16ee5554da9a2251c", size = 1550512, upload-time = "2025-12-23T03:14:04.391Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/40/2614036cdd416452f5bf98ec037f38a1afb17f327cb8e6b652d4729e0af8/pyparsing-3.3.1-py3-none-any.whl", hash = "sha256:023b5e7e5520ad96642e2c6db4cb683d3970bd640cdf7115049a6e9c3682df82", size = 121793, upload-time = "2025-12-23T03:14:02.103Z" }, +] + +[[package]] +name = "pyreadline3" +version = "3.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/49/4cea918a08f02817aabae639e3d0ac046fef9f9180518a3ad394e22da148/pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7", size = 99839, upload-time = "2024-09-19T02:40:10.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178, upload-time = "2024-09-19T02:40:08.598Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.21" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/96/804520d0850c7db98e5ccb70282e29208723f0964e88ffd9d0da2f52ea09/python_multipart-0.0.21.tar.gz", hash = "sha256:7137ebd4d3bbf70ea1622998f902b97a29434a9e8dc40eb203bbcf7c2a2cba92", size = 37196, upload-time = "2025-12-17T09:24:22.446Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/76/03af049af4dcee5d27442f71b6924f01f3efb5d2bd34f23fcd563f2cc5f5/python_multipart-0.0.21-py3-none-any.whl", hash = "sha256:cf7a6713e01c87aa35387f4774e812c4361150938d20d232800f75ffcf266090", size = 24541, upload-time = "2025-12-17T09:24:21.153Z" }, +] + +[[package]] +name = "pytorch-lightning" +version = "2.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fsspec", extra = ["http"] }, + { name = "lightning-utilities" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "torch" }, + { name = "torchmetrics" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/d7/e3963d9669758f93b07941f4e2e82a394eb3d0980e29baa4764f3bad6689/pytorch_lightning-2.6.0.tar.gz", hash = "sha256:25b0d4f05e1f33b72be0920c34d0465777fe5f623228f9d6252b4b0f685d7037", size = 658853, upload-time = "2025-11-28T09:34:13.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/eb/cc6dbfe70d15318dbce82674b1e8057cef2634ca9f9121a16b8a06c630db/pytorch_lightning-2.6.0-py3-none-any.whl", hash = "sha256:ee72cff4b8c983ecfaae8599382544bd5236d9eb300adc7dd305f359195f4e79", size = 849476, upload-time = "2025-11-28T09:34:11.271Z" }, +] + +[[package]] +name = "pytorch-metric-learning" +version = "2.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "scikit-learn" }, + { name = "torch" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/80/6e61b1a91debf4c1b47d441f9a9d7fe2aabcdd9575ed70b2811474eb95c3/pytorch-metric-learning-2.9.0.tar.gz", hash = "sha256:27a626caf5e2876a0fd666605a78cb67ef7597e25d7a68c18053dd503830701f", size = 84530, upload-time = "2025-08-17T17:11:19.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/7d/73ef5052f57b7720cad00e16598db3592a5ef4826745ffca67a2f085d4dc/pytorch_metric_learning-2.9.0-py3-none-any.whl", hash = "sha256:d51646006dc87168f00cf954785db133a4c5aac81253877248737aa42ef6432a", size = 127801, upload-time = "2025-08-17T17:11:18.185Z" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[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" }, +] + +[[package]] +name = "qwen-asr" +version = "0.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "accelerate" }, + { name = "flask" }, + { name = "gradio" }, + { name = "librosa" }, + { name = "nagisa" }, + { name = "pytz" }, + { name = "qwen-omni-utils" }, + { name = "soundfile" }, + { name = "sox" }, + { name = "soynlp" }, + { name = "transformers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/5b/56c5175d1a4d6ed8602003385570304305e4ae9c40b999d12d75a70c0561/qwen_asr-0.0.6.tar.gz", hash = "sha256:294893f2340dc2d58d1f1d1cb8ce26ac0a79f254805ab432dda2892bd5c8d13c", size = 146666, upload-time = "2026-01-30T03:24:02.697Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/12/d3027a7e4dc2eea0b12a4bf8414a7109f055004e1771666e01d8859d3ca0/qwen_asr-0.0.6-py3-none-any.whl", hash = "sha256:b9c55a38413298f3a990a4475467399daec6e8f4172363053fc42e2166c2dfd3", size = 141603, upload-time = "2026-01-30T03:24:00.82Z" }, +] + +[[package]] +name = "qwen-omni-utils" +version = "0.0.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "av" }, + { name = "librosa" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b8/b1/cc58b03b5eadddc0812cef884d013ed6cc66b09f9b0f5b45123f89dcd056/qwen_omni_utils-0.0.8.tar.gz", hash = "sha256:b5808673e1455f4115cb784a62cdc8e8616576221a01fc738610b0f9268cb33c", size = 8145, upload-time = "2025-06-12T11:02:05.411Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/b1/dcdd69246a3c3c3bd6f6ced58e2307b3afbd894c4412c29fd49dd897e562/qwen_omni_utils-0.0.8-py3-none-any.whl", hash = "sha256:c42bcc633fbfd84d565ff0de9d45fae68a6b57a9b7b97a4b77eda71a0d3ee73a", size = 9218, upload-time = "2025-06-12T11:02:03.981Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "regex" +version = "2026.1.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/86/07d5056945f9ec4590b518171c4254a5925832eb727b56d3c38a7476f316/regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5", size = 414811, upload-time = "2026-01-14T23:18:02.775Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/c9/0c80c96eab96948363d270143138d671d5731c3a692b417629bf3492a9d6/regex-2026.1.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ae6020fb311f68d753b7efa9d4b9a5d47a5d6466ea0d5e3b5a471a960ea6e4a", size = 488168, upload-time = "2026-01-14T23:14:16.129Z" }, + { url = "https://files.pythonhosted.org/packages/17/f0/271c92f5389a552494c429e5cc38d76d1322eb142fb5db3c8ccc47751468/regex-2026.1.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eddf73f41225942c1f994914742afa53dc0d01a6e20fe14b878a1b1edc74151f", size = 290636, upload-time = "2026-01-14T23:14:17.715Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f9/5f1fd077d106ca5655a0f9ff8f25a1ab55b92128b5713a91ed7134ff688e/regex-2026.1.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e8cd52557603f5c66a548f69421310886b28b7066853089e1a71ee710e1cdc1", size = 288496, upload-time = "2026-01-14T23:14:19.326Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e1/8f43b03a4968c748858ec77f746c286d81f896c2e437ccf050ebc5d3128c/regex-2026.1.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5170907244b14303edc5978f522f16c974f32d3aa92109fabc2af52411c9433b", size = 793503, upload-time = "2026-01-14T23:14:20.922Z" }, + { url = "https://files.pythonhosted.org/packages/8d/4e/a39a5e8edc5377a46a7c875c2f9a626ed3338cb3bb06931be461c3e1a34a/regex-2026.1.15-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2748c1ec0663580b4510bd89941a31560b4b439a0b428b49472a3d9944d11cd8", size = 860535, upload-time = "2026-01-14T23:14:22.405Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1c/9dce667a32a9477f7a2869c1c767dc00727284a9fa3ff5c09a5c6c03575e/regex-2026.1.15-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2f2775843ca49360508d080eaa87f94fa248e2c946bbcd963bb3aae14f333413", size = 907225, upload-time = "2026-01-14T23:14:23.897Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3c/87ca0a02736d16b6262921425e84b48984e77d8e4e572c9072ce96e66c30/regex-2026.1.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9ea2604370efc9a174c1b5dcc81784fb040044232150f7f33756049edfc9026", size = 800526, upload-time = "2026-01-14T23:14:26.039Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/647d5715aeea7c87bdcbd2f578f47b415f55c24e361e639fe8c0cc88878f/regex-2026.1.15-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dcd31594264029b57bf16f37fd7248a70b3b764ed9e0839a8f271b2d22c0785", size = 773446, upload-time = "2026-01-14T23:14:28.109Z" }, + { url = "https://files.pythonhosted.org/packages/af/89/bf22cac25cb4ba0fe6bff52ebedbb65b77a179052a9d6037136ae93f42f4/regex-2026.1.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c08c1f3e34338256732bd6938747daa3c0d5b251e04b6e43b5813e94d503076e", size = 783051, upload-time = "2026-01-14T23:14:29.929Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f4/6ed03e71dca6348a5188363a34f5e26ffd5db1404780288ff0d79513bce4/regex-2026.1.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e43a55f378df1e7a4fa3547c88d9a5a9b7113f653a66821bcea4718fe6c58763", size = 854485, upload-time = "2026-01-14T23:14:31.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/8e8560bd78caded8eb137e3e47612430a05b9a772caf60876435192d670a/regex-2026.1.15-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f82110ab962a541737bd0ce87978d4c658f06e7591ba899192e2712a517badbb", size = 762195, upload-time = "2026-01-14T23:14:32.802Z" }, + { url = "https://files.pythonhosted.org/packages/38/6b/61fc710f9aa8dfcd764fe27d37edfaa023b1a23305a0d84fccd5adb346ea/regex-2026.1.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:27618391db7bdaf87ac6c92b31e8f0dfb83a9de0075855152b720140bda177a2", size = 845986, upload-time = "2026-01-14T23:14:34.898Z" }, + { url = "https://files.pythonhosted.org/packages/fd/2e/fbee4cb93f9d686901a7ca8d94285b80405e8c34fe4107f63ffcbfb56379/regex-2026.1.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bfb0d6be01fbae8d6655c8ca21b3b72458606c4aec9bbc932db758d47aba6db1", size = 788992, upload-time = "2026-01-14T23:14:37.116Z" }, + { url = "https://files.pythonhosted.org/packages/ed/14/3076348f3f586de64b1ab75a3fbabdaab7684af7f308ad43be7ef1849e55/regex-2026.1.15-cp311-cp311-win32.whl", hash = "sha256:b10e42a6de0e32559a92f2f8dc908478cc0fa02838d7dbe764c44dca3fa13569", size = 265893, upload-time = "2026-01-14T23:14:38.426Z" }, + { url = "https://files.pythonhosted.org/packages/0f/19/772cf8b5fc803f5c89ba85d8b1870a1ca580dc482aa030383a9289c82e44/regex-2026.1.15-cp311-cp311-win_amd64.whl", hash = "sha256:e9bf3f0bbdb56633c07d7116ae60a576f846efdd86a8848f8d62b749e1209ca7", size = 277840, upload-time = "2026-01-14T23:14:39.785Z" }, + { url = "https://files.pythonhosted.org/packages/78/84/d05f61142709474da3c0853222d91086d3e1372bcdab516c6fd8d80f3297/regex-2026.1.15-cp311-cp311-win_arm64.whl", hash = "sha256:41aef6f953283291c4e4e6850607bd71502be67779586a61472beacb315c97ec", size = 270374, upload-time = "2026-01-14T23:14:41.592Z" }, + { url = "https://files.pythonhosted.org/packages/92/81/10d8cf43c807d0326efe874c1b79f22bfb0fb226027b0b19ebc26d301408/regex-2026.1.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c8fcc5793dde01641a35905d6731ee1548f02b956815f8f1cab89e515a5bdf1", size = 489398, upload-time = "2026-01-14T23:14:43.741Z" }, + { url = "https://files.pythonhosted.org/packages/90/b0/7c2a74e74ef2a7c32de724658a69a862880e3e4155cba992ba04d1c70400/regex-2026.1.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bfd876041a956e6a90ad7cdb3f6a630c07d491280bfeed4544053cd434901681", size = 291339, upload-time = "2026-01-14T23:14:45.183Z" }, + { url = "https://files.pythonhosted.org/packages/19/4d/16d0773d0c818417f4cc20aa0da90064b966d22cd62a8c46765b5bd2d643/regex-2026.1.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9250d087bc92b7d4899ccd5539a1b2334e44eee85d848c4c1aef8e221d3f8c8f", size = 289003, upload-time = "2026-01-14T23:14:47.25Z" }, + { url = "https://files.pythonhosted.org/packages/c6/e4/1fc4599450c9f0863d9406e944592d968b8d6dfd0d552a7d569e43bceada/regex-2026.1.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8a154cf6537ebbc110e24dabe53095e714245c272da9c1be05734bdad4a61aa", size = 798656, upload-time = "2026-01-14T23:14:48.77Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e6/59650d73a73fa8a60b3a590545bfcf1172b4384a7df2e7fe7b9aab4e2da9/regex-2026.1.15-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8050ba2e3ea1d8731a549e83c18d2f0999fbc99a5f6bd06b4c91449f55291804", size = 864252, upload-time = "2026-01-14T23:14:50.528Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ab/1d0f4d50a1638849a97d731364c9a80fa304fec46325e48330c170ee8e80/regex-2026.1.15-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf065240704cb8951cc04972cf107063917022511273e0969bdb34fc173456c", size = 912268, upload-time = "2026-01-14T23:14:52.952Z" }, + { url = "https://files.pythonhosted.org/packages/dd/df/0d722c030c82faa1d331d1921ee268a4e8fb55ca8b9042c9341c352f17fa/regex-2026.1.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c32bef3e7aeee75746748643667668ef941d28b003bfc89994ecf09a10f7a1b5", size = 803589, upload-time = "2026-01-14T23:14:55.182Z" }, + { url = "https://files.pythonhosted.org/packages/66/23/33289beba7ccb8b805c6610a8913d0131f834928afc555b241caabd422a9/regex-2026.1.15-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5eaa4a4c5b1906bd0d2508d68927f15b81821f85092e06f1a34a4254b0e1af3", size = 775700, upload-time = "2026-01-14T23:14:56.707Z" }, + { url = "https://files.pythonhosted.org/packages/e7/65/bf3a42fa6897a0d3afa81acb25c42f4b71c274f698ceabd75523259f6688/regex-2026.1.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:86c1077a3cc60d453d4084d5b9649065f3bf1184e22992bd322e1f081d3117fb", size = 787928, upload-time = "2026-01-14T23:14:58.312Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f5/13bf65864fc314f68cdd6d8ca94adcab064d4d39dbd0b10fef29a9da48fc/regex-2026.1.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2b091aefc05c78d286657cd4db95f2e6313375ff65dcf085e42e4c04d9c8d410", size = 858607, upload-time = "2026-01-14T23:15:00.657Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/040e589834d7a439ee43fb0e1e902bc81bd58a5ba81acffe586bb3321d35/regex-2026.1.15-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:57e7d17f59f9ebfa9667e6e5a1c0127b96b87cb9cede8335482451ed00788ba4", size = 763729, upload-time = "2026-01-14T23:15:02.248Z" }, + { url = "https://files.pythonhosted.org/packages/9b/84/6921e8129687a427edf25a34a5594b588b6d88f491320b9de5b6339a4fcb/regex-2026.1.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6c4dcdfff2c08509faa15d36ba7e5ef5fcfab25f1e8f85a0c8f45bc3a30725d", size = 850697, upload-time = "2026-01-14T23:15:03.878Z" }, + { url = "https://files.pythonhosted.org/packages/8a/87/3d06143d4b128f4229158f2de5de6c8f2485170c7221e61bf381313314b2/regex-2026.1.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf8ff04c642716a7f2048713ddc6278c5fd41faa3b9cab12607c7abecd012c22", size = 789849, upload-time = "2026-01-14T23:15:06.102Z" }, + { url = "https://files.pythonhosted.org/packages/77/69/c50a63842b6bd48850ebc7ab22d46e7a2a32d824ad6c605b218441814639/regex-2026.1.15-cp312-cp312-win32.whl", hash = "sha256:82345326b1d8d56afbe41d881fdf62f1926d7264b2fc1537f99ae5da9aad7913", size = 266279, upload-time = "2026-01-14T23:15:07.678Z" }, + { url = "https://files.pythonhosted.org/packages/f2/36/39d0b29d087e2b11fd8191e15e81cce1b635fcc845297c67f11d0d19274d/regex-2026.1.15-cp312-cp312-win_amd64.whl", hash = "sha256:4def140aa6156bc64ee9912383d4038f3fdd18fee03a6f222abd4de6357ce42a", size = 277166, upload-time = "2026-01-14T23:15:09.257Z" }, + { url = "https://files.pythonhosted.org/packages/28/32/5b8e476a12262748851fa8ab1b0be540360692325975b094e594dfebbb52/regex-2026.1.15-cp312-cp312-win_arm64.whl", hash = "sha256:c6c565d9a6e1a8d783c1948937ffc377dd5771e83bd56de8317c450a954d2056", size = 270415, upload-time = "2026-01-14T23:15:10.743Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "resampy" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numba" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/f1/34be702a69a5d272e844c98cee82351f880985cfbca0cc86378011078497/resampy-0.4.3.tar.gz", hash = "sha256:a0d1c28398f0e55994b739650afef4e3974115edbe96cd4bb81968425e916e47", size = 3080604, upload-time = "2024-03-05T20:36:08.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/b9/3b00ac340a1aab3389ebcc52c779914a44aadf7b0cb7a3bf053195735607/resampy-0.4.3-py3-none-any.whl", hash = "sha256:ad2ed64516b140a122d96704e32bc0f92b23f45419e8b8f478e5a05f83edcebd", size = 3076529, upload-time = "2024-03-05T20:36:02.439Z" }, +] + +[[package]] +name = "rich" +version = "14.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, +] + +[[package]] +name = "rotary-embedding-torch" +version = "0.6.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "einops" }, + { name = "torch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/7e/8dbb4a950a99ec2f459ec1eb73818c519b56a1fee8a9a98f7a57460a0d5a/rotary_embedding_torch-0.6.5.tar.gz", hash = "sha256:a3623274c559e0215922edc7cd14068d5a64dea1ca469e1ecd642b293ee8af78", size = 7114, upload-time = "2024-08-20T20:52:44.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/1a/d78ef65d18ed5b9b8d04e68c69ef393adecc36f189186e250089ed432d7d/rotary_embedding_torch-0.6.5-py3-none-any.whl", hash = "sha256:96c0b27b688820cbde652dd7869cf035b4f653eee53ddaf78133cf7cdd4aab19", size = 5454, upload-time = "2024-08-20T20:52:43.266Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[package]] +name = "ruamel-yaml" +version = "0.18.17" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ruamel-yaml-clib", marker = "platform_python_implementation == 'CPython'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/2b/7a1f1ebcd6b3f14febdc003e658778d81e76b40df2267904ee6b13f0c5c6/ruamel_yaml-0.18.17.tar.gz", hash = "sha256:9091cd6e2d93a3a4b157ddb8fabf348c3de7f1fb1381346d985b6b247dcd8d3c", size = 149602, upload-time = "2025-12-17T20:02:55.757Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/fe/b6045c782f1fd1ae317d2a6ca1884857ce5c20f59befe6ab25a8603c43a7/ruamel_yaml-0.18.17-py3-none-any.whl", hash = "sha256:9c8ba9eb3e793efdf924b60d521820869d5bf0cb9c6f1b82d82de8295e290b9d", size = 121594, upload-time = "2025-12-17T20:02:07.657Z" }, +] + +[[package]] +name = "ruamel-yaml-clib" +version = "0.2.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/97/60fda20e2fb54b83a61ae14648b0817c8f5d84a3821e40bfbdae1437026a/ruamel_yaml_clib-0.2.15.tar.gz", hash = "sha256:46e4cc8c43ef6a94885f72512094e482114a8a706d3c555a34ed4b0d20200600", size = 225794, upload-time = "2025-11-16T16:12:59.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/80/8ce7b9af532aa94dd83360f01ce4716264db73de6bc8efd22c32341f6658/ruamel_yaml_clib-0.2.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c583229f336682b7212a43d2fa32c30e643d3076178fb9f7a6a14dde85a2d8bd", size = 147998, upload-time = "2025-11-16T16:13:13.241Z" }, + { url = "https://files.pythonhosted.org/packages/53/09/de9d3f6b6701ced5f276d082ad0f980edf08ca67114523d1b9264cd5e2e0/ruamel_yaml_clib-0.2.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56ea19c157ed8c74b6be51b5fa1c3aff6e289a041575f0556f66e5fb848bb137", size = 132743, upload-time = "2025-11-16T16:13:14.265Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f7/73a9b517571e214fe5c246698ff3ed232f1ef863c8ae1667486625ec688a/ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5fea0932358e18293407feb921d4f4457db837b67ec1837f87074667449f9401", size = 731459, upload-time = "2025-11-16T20:22:44.338Z" }, + { url = "https://files.pythonhosted.org/packages/9b/a2/0dc0013169800f1c331a6f55b1282c1f4492a6d32660a0cf7b89e6684919/ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71831bd61fbdb7aa0399d5c4da06bea37107ab5c79ff884cc07f2450910262", size = 749289, upload-time = "2025-11-16T16:13:15.633Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ed/3fb20a1a96b8dc645d88c4072df481fe06e0289e4d528ebbdcc044ebc8b3/ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:617d35dc765715fa86f8c3ccdae1e4229055832c452d4ec20856136acc75053f", size = 777630, upload-time = "2025-11-16T16:13:16.898Z" }, + { url = "https://files.pythonhosted.org/packages/60/50/6842f4628bc98b7aa4733ab2378346e1441e150935ad3b9f3c3c429d9408/ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b45498cc81a4724a2d42273d6cfc243c0547ad7c6b87b4f774cb7bcc131c98d", size = 744368, upload-time = "2025-11-16T16:13:18.117Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b0/128ae8e19a7d794c2e36130a72b3bb650ce1dd13fb7def6cf10656437dcf/ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:def5663361f6771b18646620fca12968aae730132e104688766cf8a3b1d65922", size = 745233, upload-time = "2025-11-16T20:22:45.833Z" }, + { url = "https://files.pythonhosted.org/packages/75/05/91130633602d6ba7ce3e07f8fc865b40d2a09efd4751c740df89eed5caf9/ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:014181cdec565c8745b7cbc4de3bf2cc8ced05183d986e6d1200168e5bb59490", size = 770963, upload-time = "2025-11-16T16:13:19.344Z" }, + { url = "https://files.pythonhosted.org/packages/fd/4b/fd4542e7f33d7d1bc64cc9ac9ba574ce8cf145569d21f5f20133336cdc8c/ruamel_yaml_clib-0.2.15-cp311-cp311-win32.whl", hash = "sha256:d290eda8f6ada19e1771b54e5706b8f9807e6bb08e873900d5ba114ced13e02c", size = 102640, upload-time = "2025-11-16T16:13:20.498Z" }, + { url = "https://files.pythonhosted.org/packages/bb/eb/00ff6032c19c7537371e3119287999570867a0eafb0154fccc80e74bf57a/ruamel_yaml_clib-0.2.15-cp311-cp311-win_amd64.whl", hash = "sha256:bdc06ad71173b915167702f55d0f3f027fc61abd975bd308a0968c02db4a4c3e", size = 121996, upload-time = "2025-11-16T16:13:21.855Z" }, + { url = "https://files.pythonhosted.org/packages/72/4b/5fde11a0722d676e469d3d6f78c6a17591b9c7e0072ca359801c4bd17eee/ruamel_yaml_clib-0.2.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb15a2e2a90c8475df45c0949793af1ff413acfb0a716b8b94e488ea95ce7cff", size = 149088, upload-time = "2025-11-16T16:13:22.836Z" }, + { url = "https://files.pythonhosted.org/packages/85/82/4d08ac65ecf0ef3b046421985e66301a242804eb9a62c93ca3437dc94ee0/ruamel_yaml_clib-0.2.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:64da03cbe93c1e91af133f5bec37fd24d0d4ba2418eaf970d7166b0a26a148a2", size = 134553, upload-time = "2025-11-16T16:13:24.151Z" }, + { url = "https://files.pythonhosted.org/packages/b9/cb/22366d68b280e281a932403b76da7a988108287adff2bfa5ce881200107a/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f6d3655e95a80325b84c4e14c080b2470fe4f33b6846f288379ce36154993fb1", size = 737468, upload-time = "2025-11-16T20:22:47.335Z" }, + { url = "https://files.pythonhosted.org/packages/71/73/81230babf8c9e33770d43ed9056f603f6f5f9665aea4177a2c30ae48e3f3/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71845d377c7a47afc6592aacfea738cc8a7e876d586dfba814501d8c53c1ba60", size = 753349, upload-time = "2025-11-16T16:13:26.269Z" }, + { url = "https://files.pythonhosted.org/packages/61/62/150c841f24cda9e30f588ef396ed83f64cfdc13b92d2f925bb96df337ba9/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e5499db1ccbc7f4b41f0565e4f799d863ea720e01d3e99fa0b7b5fcd7802c9", size = 788211, upload-time = "2025-11-16T16:13:27.441Z" }, + { url = "https://files.pythonhosted.org/packages/30/93/e79bd9cbecc3267499d9ead919bd61f7ddf55d793fb5ef2b1d7d92444f35/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4b293a37dc97e2b1e8a1aec62792d1e52027087c8eea4fc7b5abd2bdafdd6642", size = 743203, upload-time = "2025-11-16T16:13:28.671Z" }, + { url = "https://files.pythonhosted.org/packages/8d/06/1eb640065c3a27ce92d76157f8efddb184bd484ed2639b712396a20d6dce/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:512571ad41bba04eac7268fe33f7f4742210ca26a81fe0c75357fa682636c690", size = 747292, upload-time = "2025-11-16T20:22:48.584Z" }, + { url = "https://files.pythonhosted.org/packages/a5/21/ee353e882350beab65fcc47a91b6bdc512cace4358ee327af2962892ff16/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5e9f630c73a490b758bf14d859a39f375e6999aea5ddd2e2e9da89b9953486a", size = 771624, upload-time = "2025-11-16T16:13:29.853Z" }, + { url = "https://files.pythonhosted.org/packages/57/34/cc1b94057aa867c963ecf9ea92ac59198ec2ee3a8d22a126af0b4d4be712/ruamel_yaml_clib-0.2.15-cp312-cp312-win32.whl", hash = "sha256:f4421ab780c37210a07d138e56dd4b51f8642187cdfb433eb687fe8c11de0144", size = 100342, upload-time = "2025-11-16T16:13:31.067Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e5/8925a4208f131b218f9a7e459c0d6fcac8324ae35da269cb437894576366/ruamel_yaml_clib-0.2.15-cp312-cp312-win_amd64.whl", hash = "sha256:2b216904750889133d9222b7b873c199d48ecbb12912aca78970f84a5aa1a4bc", size = 119013, upload-time = "2025-11-16T16:13:32.164Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/50/0a/1914efb7903174b381ee2ffeebb4253e729de57f114e63595114c8ca451f/ruff-0.14.13.tar.gz", hash = "sha256:83cd6c0763190784b99650a20fec7633c59f6ebe41c5cc9d45ee42749563ad47", size = 6059504, upload-time = "2026-01-15T20:15:16.918Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/ae/0deefbc65ca74b0ab1fd3917f94dc3b398233346a74b8bbb0a916a1a6bf6/ruff-0.14.13-py3-none-linux_armv6l.whl", hash = "sha256:76f62c62cd37c276cb03a275b198c7c15bd1d60c989f944db08a8c1c2dbec18b", size = 13062418, upload-time = "2026-01-15T20:14:50.779Z" }, + { url = "https://files.pythonhosted.org/packages/47/df/5916604faa530a97a3c154c62a81cb6b735c0cb05d1e26d5ad0f0c8ac48a/ruff-0.14.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:914a8023ece0528d5cc33f5a684f5f38199bbb566a04815c2c211d8f40b5d0ed", size = 13442344, upload-time = "2026-01-15T20:15:07.94Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f3/e0e694dd69163c3a1671e102aa574a50357536f18a33375050334d5cd517/ruff-0.14.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d24899478c35ebfa730597a4a775d430ad0d5631b8647a3ab368c29b7e7bd063", size = 12354720, upload-time = "2026-01-15T20:15:09.854Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e8/67f5fcbbaee25e8fc3b56cc33e9892eca7ffe09f773c8e5907757a7e3bdb/ruff-0.14.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9aaf3870f14d925bbaf18b8a2347ee0ae7d95a2e490e4d4aea6813ed15ebc80e", size = 12774493, upload-time = "2026-01-15T20:15:20.908Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ce/d2e9cb510870b52a9565d885c0d7668cc050e30fa2c8ac3fb1fda15c083d/ruff-0.14.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac5b7f63dd3b27cc811850f5ffd8fff845b00ad70e60b043aabf8d6ecc304e09", size = 12815174, upload-time = "2026-01-15T20:15:05.74Z" }, + { url = "https://files.pythonhosted.org/packages/88/00/c38e5da58beebcf4fa32d0ddd993b63dfacefd02ab7922614231330845bf/ruff-0.14.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d2b1097750d90ba82ce4ba676e85230a0ed694178ca5e61aa9b459970b3eb9", size = 13680909, upload-time = "2026-01-15T20:15:14.537Z" }, + { url = "https://files.pythonhosted.org/packages/61/61/cd37c9dd5bd0a3099ba79b2a5899ad417d8f3b04038810b0501a80814fd7/ruff-0.14.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d0bf87705acbbcb8d4c24b2d77fbb73d40210a95c3903b443cd9e30824a5032", size = 15144215, upload-time = "2026-01-15T20:15:22.886Z" }, + { url = "https://files.pythonhosted.org/packages/56/8a/85502d7edbf98c2df7b8876f316c0157359165e16cdf98507c65c8d07d3d/ruff-0.14.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3eb5da8e2c9e9f13431032fdcbe7681de9ceda5835efee3269417c13f1fed5c", size = 14706067, upload-time = "2026-01-15T20:14:48.271Z" }, + { url = "https://files.pythonhosted.org/packages/7e/2f/de0df127feb2ee8c1e54354dc1179b4a23798f0866019528c938ba439aca/ruff-0.14.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:642442b42957093811cd8d2140dfadd19c7417030a7a68cf8d51fcdd5f217427", size = 14133916, upload-time = "2026-01-15T20:14:57.357Z" }, + { url = "https://files.pythonhosted.org/packages/0d/77/9b99686bb9fe07a757c82f6f95e555c7a47801a9305576a9c67e0a31d280/ruff-0.14.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4acdf009f32b46f6e8864af19cbf6841eaaed8638e65c8dac845aea0d703c841", size = 13859207, upload-time = "2026-01-15T20:14:55.111Z" }, + { url = "https://files.pythonhosted.org/packages/7d/46/2bdcb34a87a179a4d23022d818c1c236cb40e477faf0d7c9afb6813e5876/ruff-0.14.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:591a7f68860ea4e003917d19b5c4f5ac39ff558f162dc753a2c5de897fd5502c", size = 14043686, upload-time = "2026-01-15T20:14:52.841Z" }, + { url = "https://files.pythonhosted.org/packages/1a/a9/5c6a4f56a0512c691cf143371bcf60505ed0f0860f24a85da8bd123b2bf1/ruff-0.14.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:774c77e841cc6e046fc3e91623ce0903d1cd07e3a36b1a9fe79b81dab3de506b", size = 12663837, upload-time = "2026-01-15T20:15:18.921Z" }, + { url = "https://files.pythonhosted.org/packages/fe/bb/b920016ece7651fa7fcd335d9d199306665486694d4361547ccb19394c44/ruff-0.14.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:61f4e40077a1248436772bb6512db5fc4457fe4c49e7a94ea7c5088655dd21ae", size = 12805867, upload-time = "2026-01-15T20:14:59.272Z" }, + { url = "https://files.pythonhosted.org/packages/7d/b3/0bd909851e5696cd21e32a8fc25727e5f58f1934b3596975503e6e85415c/ruff-0.14.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6d02f1428357fae9e98ac7aa94b7e966fd24151088510d32cf6f902d6c09235e", size = 13208528, upload-time = "2026-01-15T20:15:03.732Z" }, + { url = "https://files.pythonhosted.org/packages/3b/3b/e2d94cb613f6bbd5155a75cbe072813756363eba46a3f2177a1fcd0cd670/ruff-0.14.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e399341472ce15237be0c0ae5fbceca4b04cd9bebab1a2b2c979e015455d8f0c", size = 13929242, upload-time = "2026-01-15T20:15:11.918Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c5/abd840d4132fd51a12f594934af5eba1d5d27298a6f5b5d6c3be45301caf/ruff-0.14.13-py3-none-win32.whl", hash = "sha256:ef720f529aec113968b45dfdb838ac8934e519711da53a0456038a0efecbd680", size = 12919024, upload-time = "2026-01-15T20:14:43.647Z" }, + { url = "https://files.pythonhosted.org/packages/c2/55/6384b0b8ce731b6e2ade2b5449bf07c0e4c31e8a2e68ea65b3bafadcecc5/ruff-0.14.13-py3-none-win_amd64.whl", hash = "sha256:6070bd026e409734b9257e03e3ef18c6e1a216f0435c6751d7a8ec69cb59abef", size = 14097887, upload-time = "2026-01-15T20:15:01.48Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e1/7348090988095e4e39560cfc2f7555b1b2a7357deba19167b600fdf5215d/ruff-0.14.13-py3-none-win_arm64.whl", hash = "sha256:7ab819e14f1ad9fe39f246cfcc435880ef7a9390d81a2b6ac7e01039083dd247", size = 13080224, upload-time = "2026-01-15T20:14:45.853Z" }, +] + +[[package]] +name = "safehttpx" +version = "0.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/d1/4282284d9cf1ee873607a46442da977fc3c985059315ab23610be31d5885/safehttpx-0.1.7.tar.gz", hash = "sha256:db201c0978c41eddb8bb480f3eee59dd67304fdd91646035e9d9a720049a9d23", size = 10385, upload-time = "2025-10-24T18:30:09.783Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/a3/0f0b7d78e2f1eb9e8e1afbff1d2bff8d60144aee17aca51c065b516743dd/safehttpx-0.1.7-py3-none-any.whl", hash = "sha256:c4f4a162db6993464d7ca3d7cc4af0ffc6515a606dfd220b9f82c6945d869cde", size = 8959, upload-time = "2025-10-24T18:30:08.733Z" }, +] + +[[package]] +name = "safetensors" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" }, + { url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" }, + { url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" }, + { url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" }, + { url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" }, + { url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" }, +] + +[[package]] +name = "samplerate" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/9c/6a13fbc59b1ceaef194a53d3aeafa983057133fb13660c423854184eafa7/samplerate-0.1.0.tar.gz", hash = "sha256:75ef725e6cd9c4545569caf4c47147beab7b53b2c36e5122e8c285d348f88847", size = 4044998, upload-time = "2017-02-24T00:01:56.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/3c/4c1aa376332d18e708dcc3289e4dbdd2c508bcac1b8eb1b35b73092fa00f/samplerate-0.1.0-py2.py3-none-any.whl", hash = "sha256:f55e5c9d0a8ba3c82a53b7d9c34a2d145439c61166a7f310efaec88f2781b8f8", size = 4028658, upload-time = "2017-02-24T00:01:44.947Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/92/53ea2181da8ac6bf27170191028aee7251f8f841f8d3edbfdcaf2008fde9/scikit_learn-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:146b4d36f800c013d267b29168813f7a03a43ecd2895d04861f1240b564421da", size = 8595835, upload-time = "2025-12-10T07:07:39.385Z" }, + { url = "https://files.pythonhosted.org/packages/01/18/d154dc1638803adf987910cdd07097d9c526663a55666a97c124d09fb96a/scikit_learn-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f984ca4b14914e6b4094c5d52a32ea16b49832c03bd17a110f004db3c223e8e1", size = 8080381, upload-time = "2025-12-10T07:07:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/44/226142fcb7b7101e64fdee5f49dbe6288d4c7af8abf593237b70fca080a4/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e30adb87f0cc81c7690a84f7932dd66be5bac57cfe16b91cb9151683a4a2d3b", size = 8799632, upload-time = "2025-12-10T07:07:43.899Z" }, + { url = "https://files.pythonhosted.org/packages/36/4d/4a67f30778a45d542bbea5db2dbfa1e9e100bf9ba64aefe34215ba9f11f6/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ada8121bcb4dac28d930febc791a69f7cb1673c8495e5eee274190b73a4559c1", size = 9103788, upload-time = "2025-12-10T07:07:45.982Z" }, + { url = "https://files.pythonhosted.org/packages/89/3c/45c352094cfa60050bcbb967b1faf246b22e93cb459f2f907b600f2ceda5/scikit_learn-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:c57b1b610bd1f40ba43970e11ce62821c2e6569e4d74023db19c6b26f246cb3b", size = 8081706, upload-time = "2025-12-10T07:07:48.111Z" }, + { url = "https://files.pythonhosted.org/packages/3d/46/5416595bb395757f754feb20c3d776553a386b661658fb21b7c814e89efe/scikit_learn-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:2838551e011a64e3053ad7618dda9310175f7515f1742fa2d756f7c874c05961", size = 7688451, upload-time = "2025-12-10T07:07:49.873Z" }, + { url = "https://files.pythonhosted.org/packages/90/74/e6a7cc4b820e95cc38cf36cd74d5aa2b42e8ffc2d21fe5a9a9c45c1c7630/scikit_learn-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5fb63362b5a7ddab88e52b6dbb47dac3fd7dafeee740dc6c8d8a446ddedade8e", size = 8548242, upload-time = "2025-12-10T07:07:51.568Z" }, + { url = "https://files.pythonhosted.org/packages/49/d8/9be608c6024d021041c7f0b3928d4749a706f4e2c3832bbede4fb4f58c95/scikit_learn-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5025ce924beccb28298246e589c691fe1b8c1c96507e6d27d12c5fadd85bfd76", size = 8079075, upload-time = "2025-12-10T07:07:53.697Z" }, + { url = "https://files.pythonhosted.org/packages/dd/47/f187b4636ff80cc63f21cd40b7b2d177134acaa10f6bb73746130ee8c2e5/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4496bb2cf7a43ce1a2d7524a79e40bc5da45cf598dbf9545b7e8316ccba47bb4", size = 8660492, upload-time = "2025-12-10T07:07:55.574Z" }, + { url = "https://files.pythonhosted.org/packages/97/74/b7a304feb2b49df9fafa9382d4d09061a96ee9a9449a7cbea7988dda0828/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0bcfe4d0d14aec44921545fd2af2338c7471de9cb701f1da4c9d85906ab847a", size = 8931904, upload-time = "2025-12-10T07:07:57.666Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c4/0ab22726a04ede56f689476b760f98f8f46607caecff993017ac1b64aa5d/scikit_learn-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:35c007dedb2ffe38fe3ee7d201ebac4a2deccd2408e8621d53067733e3c74809", size = 8019359, upload-time = "2025-12-10T07:07:59.838Z" }, + { url = "https://files.pythonhosted.org/packages/24/90/344a67811cfd561d7335c1b96ca21455e7e472d281c3c279c4d3f2300236/scikit_learn-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:8c497fff237d7b4e07e9ef1a640887fa4fb765647f86fbe00f969ff6280ce2bb", size = 7641898, upload-time = "2025-12-10T07:08:01.36Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/3e/9cca699f3486ce6bc12ff46dc2031f1ec8eb9ccc9a320fdaf925f1417426/scipy-1.17.0.tar.gz", hash = "sha256:2591060c8e648d8b96439e111ac41fd8342fdeff1876be2e19dea3fe8930454e", size = 30396830, upload-time = "2026-01-10T21:34:23.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/4b/c89c131aa87cad2b77a54eb0fb94d633a842420fa7e919dc2f922037c3d8/scipy-1.17.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:2abd71643797bd8a106dff97894ff7869eeeb0af0f7a5ce02e4227c6a2e9d6fd", size = 31381316, upload-time = "2026-01-10T21:24:33.42Z" }, + { url = "https://files.pythonhosted.org/packages/5e/5f/a6b38f79a07d74989224d5f11b55267714707582908a5f1ae854cf9a9b84/scipy-1.17.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:ef28d815f4d2686503e5f4f00edc387ae58dfd7a2f42e348bb53359538f01558", size = 27966760, upload-time = "2026-01-10T21:24:38.911Z" }, + { url = "https://files.pythonhosted.org/packages/c1/20/095ad24e031ee8ed3c5975954d816b8e7e2abd731e04f8be573de8740885/scipy-1.17.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:272a9f16d6bb4667e8b50d25d71eddcc2158a214df1b566319298de0939d2ab7", size = 20138701, upload-time = "2026-01-10T21:24:43.249Z" }, + { url = "https://files.pythonhosted.org/packages/89/11/4aad2b3858d0337756f3323f8960755704e530b27eb2a94386c970c32cbe/scipy-1.17.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:7204fddcbec2fe6598f1c5fdf027e9f259106d05202a959a9f1aecf036adc9f6", size = 22480574, upload-time = "2026-01-10T21:24:47.266Z" }, + { url = "https://files.pythonhosted.org/packages/85/bd/f5af70c28c6da2227e510875cadf64879855193a687fb19951f0f44cfd6b/scipy-1.17.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc02c37a5639ee67d8fb646ffded6d793c06c5622d36b35cfa8fe5ececb8f042", size = 32862414, upload-time = "2026-01-10T21:24:52.566Z" }, + { url = "https://files.pythonhosted.org/packages/ef/df/df1457c4df3826e908879fe3d76bc5b6e60aae45f4ee42539512438cfd5d/scipy-1.17.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dac97a27520d66c12a34fd90a4fe65f43766c18c0d6e1c0a80f114d2260080e4", size = 35112380, upload-time = "2026-01-10T21:24:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/5f/bb/88e2c16bd1dd4de19d80d7c5e238387182993c2fb13b4b8111e3927ad422/scipy-1.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ebb7446a39b3ae0fe8f416a9a3fdc6fba3f11c634f680f16a239c5187bc487c0", size = 34922676, upload-time = "2026-01-10T21:25:04.287Z" }, + { url = "https://files.pythonhosted.org/packages/02/ba/5120242cc735f71fc002cff0303d536af4405eb265f7c60742851e7ccfe9/scipy-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:474da16199f6af66601a01546144922ce402cb17362e07d82f5a6cf8f963e449", size = 37507599, upload-time = "2026-01-10T21:25:09.851Z" }, + { url = "https://files.pythonhosted.org/packages/52/c8/08629657ac6c0da198487ce8cd3de78e02cfde42b7f34117d56a3fe249dc/scipy-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:255c0da161bd7b32a6c898e7891509e8a9289f0b1c6c7d96142ee0d2b114c2ea", size = 36380284, upload-time = "2026-01-10T21:25:15.632Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4a/465f96d42c6f33ad324a40049dfd63269891db9324aa66c4a1c108c6f994/scipy-1.17.0-cp311-cp311-win_arm64.whl", hash = "sha256:85b0ac3ad17fa3be50abd7e69d583d98792d7edc08367e01445a1e2076005379", size = 24370427, upload-time = "2026-01-10T21:25:20.514Z" }, + { url = "https://files.pythonhosted.org/packages/0b/11/7241a63e73ba5a516f1930ac8d5b44cbbfabd35ac73a2d08ca206df007c4/scipy-1.17.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:0d5018a57c24cb1dd828bcf51d7b10e65986d549f52ef5adb6b4d1ded3e32a57", size = 31364580, upload-time = "2026-01-10T21:25:25.717Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1d/5057f812d4f6adc91a20a2d6f2ebcdb517fdbc87ae3acc5633c9b97c8ba5/scipy-1.17.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:88c22af9e5d5a4f9e027e26772cc7b5922fab8bcc839edb3ae33de404feebd9e", size = 27969012, upload-time = "2026-01-10T21:25:30.921Z" }, + { url = "https://files.pythonhosted.org/packages/e3/21/f6ec556c1e3b6ec4e088da667d9987bb77cc3ab3026511f427dc8451187d/scipy-1.17.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f3cd947f20fe17013d401b64e857c6b2da83cae567adbb75b9dcba865abc66d8", size = 20140691, upload-time = "2026-01-10T21:25:34.802Z" }, + { url = "https://files.pythonhosted.org/packages/7a/fe/5e5ad04784964ba964a96f16c8d4676aa1b51357199014dce58ab7ec5670/scipy-1.17.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e8c0b331c2c1f531eb51f1b4fc9ba709521a712cce58f1aa627bc007421a5306", size = 22463015, upload-time = "2026-01-10T21:25:39.277Z" }, + { url = "https://files.pythonhosted.org/packages/4a/69/7c347e857224fcaf32a34a05183b9d8a7aca25f8f2d10b8a698b8388561a/scipy-1.17.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5194c445d0a1c7a6c1a4a4681b6b7c71baad98ff66d96b949097e7513c9d6742", size = 32724197, upload-time = "2026-01-10T21:25:44.084Z" }, + { url = "https://files.pythonhosted.org/packages/d1/fe/66d73b76d378ba8cc2fe605920c0c75092e3a65ae746e1e767d9d020a75a/scipy-1.17.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9eeb9b5f5997f75507814ed9d298ab23f62cf79f5a3ef90031b1ee2506abdb5b", size = 35009148, upload-time = "2026-01-10T21:25:50.591Z" }, + { url = "https://files.pythonhosted.org/packages/af/07/07dec27d9dc41c18d8c43c69e9e413431d20c53a0339c388bcf72f353c4b/scipy-1.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:40052543f7bbe921df4408f46003d6f01c6af109b9e2c8a66dd1cf6cf57f7d5d", size = 34798766, upload-time = "2026-01-10T21:25:59.41Z" }, + { url = "https://files.pythonhosted.org/packages/81/61/0470810c8a093cdacd4ba7504b8a218fd49ca070d79eca23a615f5d9a0b0/scipy-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0cf46c8013fec9d3694dc572f0b54100c28405d55d3e2cb15e2895b25057996e", size = 37405953, upload-time = "2026-01-10T21:26:07.75Z" }, + { url = "https://files.pythonhosted.org/packages/92/ce/672ed546f96d5d41ae78c4b9b02006cedd0b3d6f2bf5bb76ea455c320c28/scipy-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:0937a0b0d8d593a198cededd4c439a0ea216a3f36653901ea1f3e4be949056f8", size = 36328121, upload-time = "2026-01-10T21:26:16.509Z" }, + { url = "https://files.pythonhosted.org/packages/9d/21/38165845392cae67b61843a52c6455d47d0cc2a40dd495c89f4362944654/scipy-1.17.0-cp312-cp312-win_arm64.whl", hash = "sha256:f603d8a5518c7426414d1d8f82e253e454471de682ce5e39c29adb0df1efb86b", size = 24314368, upload-time = "2026-01-10T21:26:23.087Z" }, +] + +[[package]] +name = "semantic-version" +version = "2.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/31/f2289ce78b9b473d582568c234e104d2a342fd658cc288a7553d83bb8595/semantic_version-2.10.0.tar.gz", hash = "sha256:bdabb6d336998cbb378d4b9db3a4b56a1e3235701dc05ea2690d9a997ed5041c", size = 52289, upload-time = "2022-05-26T13:35:23.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/23/8146aad7d88f4fcb3a6218f41a60f6c2d4e3a72de72da1825dc7c8f7877c/semantic_version-2.10.0-py2.py3-none-any.whl", hash = "sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177", size = 15552, upload-time = "2022-05-26T13:35:21.206Z" }, +] + +[[package]] +name = "semver" +version = "3.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/d1/d3159231aec234a59dd7d601e9dd9fe96f3afff15efd33c1070019b26132/semver-3.0.4.tar.gz", hash = "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602", size = 269730, upload-time = "2025-01-24T13:19:27.617Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746", size = 17912, upload-time = "2025-01-24T13:19:24.949Z" }, +] + +[[package]] +name = "sentencepiece" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/15/2e7a025fc62d764b151ae6d0f2a92f8081755ebe8d4a64099accc6f77ba6/sentencepiece-0.2.1.tar.gz", hash = "sha256:8138cec27c2f2282f4a34d9a016e3374cd40e5c6e9cb335063db66a0a3b71fad", size = 3228515, upload-time = "2025-08-12T07:00:51.718Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/15/46afbab00733d81788b64be430ca1b93011bb9388527958e26cc31832de5/sentencepiece-0.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6356d0986b8b8dc351b943150fcd81a1c6e6e4d439772e8584c64230e58ca987", size = 1942560, upload-time = "2025-08-12T06:59:25.82Z" }, + { url = "https://files.pythonhosted.org/packages/fa/79/7c01b8ef98a0567e9d84a4e7a910f8e7074fcbf398a5cd76f93f4b9316f9/sentencepiece-0.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8f8ba89a3acb3dc1ae90f65ec1894b0b9596fdb98ab003ff38e058f898b39bc7", size = 1325385, upload-time = "2025-08-12T06:59:27.722Z" }, + { url = "https://files.pythonhosted.org/packages/bb/88/2b41e07bd24f33dcf2f18ec3b74247aa4af3526bad8907b8727ea3caba03/sentencepiece-0.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:02593eca45440ef39247cee8c47322a34bdcc1d8ae83ad28ba5a899a2cf8d79a", size = 1253319, upload-time = "2025-08-12T06:59:29.306Z" }, + { url = "https://files.pythonhosted.org/packages/a0/54/38a1af0c6210a3c6f95aa46d23d6640636d020fba7135cd0d9a84ada05a7/sentencepiece-0.2.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a0d15781a171d188b661ae4bde1d998c303f6bd8621498c50c671bd45a4798e", size = 1316162, upload-time = "2025-08-12T06:59:30.914Z" }, + { url = "https://files.pythonhosted.org/packages/ef/66/fb191403ade791ad2c3c1e72fe8413e63781b08cfa3aa4c9dfc536d6e795/sentencepiece-0.2.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f5a3e0d9f445ed9d66c0fec47d4b23d12cfc858b407a03c194c1b26c2ac2a63", size = 1387785, upload-time = "2025-08-12T06:59:32.491Z" }, + { url = "https://files.pythonhosted.org/packages/a9/2d/3bd9b08e70067b2124518b308db6a84a4f8901cc8a4317e2e4288cdd9b4d/sentencepiece-0.2.1-cp311-cp311-win32.whl", hash = "sha256:6d297a1748d429ba8534eebe5535448d78b8acc32d00a29b49acf28102eeb094", size = 999555, upload-time = "2025-08-12T06:59:34.475Z" }, + { url = "https://files.pythonhosted.org/packages/32/b8/f709977f5fda195ae1ea24f24e7c581163b6f142b1005bc3d0bbfe4d7082/sentencepiece-0.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:82d9ead6591015f009cb1be1cb1c015d5e6f04046dbb8c9588b931e869a29728", size = 1054617, upload-time = "2025-08-12T06:59:36.461Z" }, + { url = "https://files.pythonhosted.org/packages/7a/40/a1fc23be23067da0f703709797b464e8a30a1c78cc8a687120cd58d4d509/sentencepiece-0.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:39f8651bd10974eafb9834ce30d9bcf5b73e1fc798a7f7d2528f9820ca86e119", size = 1033877, upload-time = "2025-08-12T06:59:38.391Z" }, + { url = "https://files.pythonhosted.org/packages/4a/be/32ce495aa1d0e0c323dcb1ba87096037358edee539cac5baf8755a6bd396/sentencepiece-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57cae326c8727de58c85977b175af132a7138d84c764635d7e71bbee7e774133", size = 1943152, upload-time = "2025-08-12T06:59:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/88/7e/ff23008899a58678e98c6ff592bf4d368eee5a71af96d0df6b38a039dd4f/sentencepiece-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:56dd39a3c4d6493db3cdca7e8cc68c6b633f0d4195495cbadfcf5af8a22d05a6", size = 1325651, upload-time = "2025-08-12T06:59:41.536Z" }, + { url = "https://files.pythonhosted.org/packages/19/84/42eb3ce4796777a1b5d3699dfd4dca85113e68b637f194a6c8d786f16a04/sentencepiece-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d9381351182ff9888cc80e41c632e7e274b106f450de33d67a9e8f6043da6f76", size = 1253645, upload-time = "2025-08-12T06:59:42.903Z" }, + { url = "https://files.pythonhosted.org/packages/89/fa/d3d5ebcba3cb9e6d3775a096251860c41a6bc53a1b9461151df83fe93255/sentencepiece-0.2.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99f955df238021bf11f0fc37cdb54fd5e5b5f7fd30ecc3d93fb48b6815437167", size = 1316273, upload-time = "2025-08-12T06:59:44.476Z" }, + { url = "https://files.pythonhosted.org/packages/04/88/14f2f4a2b922d8b39be45bf63d79e6cd3a9b2f248b2fcb98a69b12af12f5/sentencepiece-0.2.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cdfecef430d985f1c2bcbfff3defd1d95dae876fbd0173376012d2d7d24044b", size = 1387881, upload-time = "2025-08-12T06:59:46.09Z" }, + { url = "https://files.pythonhosted.org/packages/fd/b8/903e5ccb77b4ef140605d5d71b4f9e0ad95d456d6184688073ed11712809/sentencepiece-0.2.1-cp312-cp312-win32.whl", hash = "sha256:a483fd29a34c3e34c39ac5556b0a90942bec253d260235729e50976f5dba1068", size = 999540, upload-time = "2025-08-12T06:59:48.023Z" }, + { url = "https://files.pythonhosted.org/packages/2d/81/92df5673c067148c2545b1bfe49adfd775bcc3a169a047f5a0e6575ddaca/sentencepiece-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:4cdc7c36234fda305e85c32949c5211faaf8dd886096c7cea289ddc12a2d02de", size = 1054671, upload-time = "2025-08-12T06:59:49.895Z" }, + { url = "https://files.pythonhosted.org/packages/fe/02/c5e3bc518655d714622bec87d83db9cdba1cd0619a4a04e2109751c4f47f/sentencepiece-0.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:daeb5e9e9fcad012324807856113708614d534f596d5008638eb9b40112cd9e4", size = 1033923, upload-time = "2025-08-12T06:59:51.952Z" }, +] + +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "socksio" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/5c/48a7d9495be3d1c651198fd99dbb6ce190e2274d0f28b9051307bdec6b85/socksio-1.0.0.tar.gz", hash = "sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac", size = 19055, upload-time = "2020-04-17T15:50:34.664Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/c3/6eeb6034408dac0fa653d126c9204ade96b819c936e136c5e8a6897eee9c/socksio-1.0.0-py3-none-any.whl", hash = "sha256:95dc1f15f9b34e8d7b16f06d74b8ccf48f609af32ab33c608d08761c5dcbb1f3", size = 12763, upload-time = "2020-04-17T15:50:31.878Z" }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + +[[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 = "sox" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/a2/d8e0d8fd7abf509ead4a2cb0fb24e5758b5330166bf9223d5cb9f98a7e8d/sox-1.5.0.tar.gz", hash = "sha256:12c7be5bb1f548d891fe11e82c08cf5f1a1d74e225298f60082e5aeb2469ada0", size = 63905, upload-time = "2024-03-20T16:59:37.385Z" } + +[[package]] +name = "soxr" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/7e/f4b461944662ad75036df65277d6130f9411002bfb79e9df7dff40a31db9/soxr-1.0.0.tar.gz", hash = "sha256:e07ee6c1d659bc6957034f4800c60cb8b98de798823e34d2a2bba1caa85a4509", size = 171415, upload-time = "2025-09-07T13:22:21.317Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/ce/a3262bc8733d3a4ce5f660ed88c3d97f4b12658b0909e71334cba1721dcb/soxr-1.0.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:28e19d74a5ef45c0d7000f3c70ec1719e89077379df2a1215058914d9603d2d8", size = 206739, upload-time = "2025-09-07T13:21:54.572Z" }, + { url = "https://files.pythonhosted.org/packages/64/dc/e8cbd100b652697cc9865dbed08832e7e135ff533f453eb6db9e6168d153/soxr-1.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8dc69fc18884e53b72f6141fdf9d80997edbb4fec9dc2942edcb63abbe0d023", size = 165233, upload-time = "2025-09-07T13:21:55.887Z" }, + { url = "https://files.pythonhosted.org/packages/75/12/4b49611c9ba5e9fe6f807d0a83352516808e8e573f8b4e712fc0c17f3363/soxr-1.0.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f15450e6f65f22f02fcd4c5a9219c873b1e583a73e232805ff160c759a6b586", size = 208867, upload-time = "2025-09-07T13:21:57.076Z" }, + { url = "https://files.pythonhosted.org/packages/cc/70/92146ab970a3ef8c43ac160035b1e52fde5417f89adb10572f7e788d9596/soxr-1.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f73f57452f9df37b4de7a4052789fcbd474a5b28f38bba43278ae4b489d4384", size = 242633, upload-time = "2025-09-07T13:21:58.621Z" }, + { url = "https://files.pythonhosted.org/packages/b5/a7/628479336206959463d08260bffed87905e7ba9e3bd83ca6b405a0736e94/soxr-1.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:9f417c3d69236051cf5a1a7bad7c4bff04eb3d8fcaa24ac1cb06e26c8d48d8dc", size = 173814, upload-time = "2025-09-07T13:21:59.798Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c7/f92b81f1a151c13afb114f57799b86da9330bec844ea5a0d3fe6a8732678/soxr-1.0.0-cp312-abi3-macosx_10_14_x86_64.whl", hash = "sha256:abecf4e39017f3fadb5e051637c272ae5778d838e5c3926a35db36a53e3a607f", size = 205508, upload-time = "2025-09-07T13:22:01.252Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1d/c945fea9d83ea1f2be9d116b3674dbaef26ed090374a77c394b31e3b083b/soxr-1.0.0-cp312-abi3-macosx_11_0_arm64.whl", hash = "sha256:e973d487ee46aa8023ca00a139db6e09af053a37a032fe22f9ff0cc2e19c94b4", size = 163568, upload-time = "2025-09-07T13:22:03.558Z" }, + { url = "https://files.pythonhosted.org/packages/b5/80/10640970998a1d2199bef6c4d92205f36968cddaf3e4d0e9fe35ddd405bd/soxr-1.0.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8ce273cca101aff3d8c387db5a5a41001ba76ef1837883438d3c652507a9ccc", size = 204707, upload-time = "2025-09-07T13:22:05.125Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/2726603c13c2126cb8ded9e57381b7377f4f0df6ba4408e1af5ddbfdc3dd/soxr-1.0.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8f2a69686f2856d37823bbb7b78c3d44904f311fe70ba49b893af11d6b6047b", size = 238032, upload-time = "2025-09-07T13:22:06.428Z" }, + { url = "https://files.pythonhosted.org/packages/ce/04/530252227f4d0721a5524a936336485dfb429bb206a66baf8e470384f4a2/soxr-1.0.0-cp312-abi3-win_amd64.whl", hash = "sha256:2a3b77b115ae7c478eecdbd060ed4f61beda542dfb70639177ac263aceda42a2", size = 172070, upload-time = "2025-09-07T13:22:07.62Z" }, +] + +[[package]] +name = "soynlp" +version = "0.0.493" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "psutil" }, + { name = "scikit-learn" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/05/fb3f0c9248e60a0224e77198c31517b93b92d6060873b641caf474d42993/soynlp-0.0.493.tar.gz", hash = "sha256:4b20b825edbcadb9ebd494de54d8ca50b0ee317fe58d090e8b509d733422136a", size = 405393, upload-time = "2019-08-25T05:28:00.755Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/50/6913dc52a86a6b189419e59f9eef1b8d599cffb6f44f7bb91854165fc603/soynlp-0.0.493-py3-none-any.whl", hash = "sha256:2aed0ced1f0f74f7bdd0bdc24a979c5cb9ee4a28393642db52a83d174ed65b7a", size = 416753, upload-time = "2019-08-25T05:27:54.56Z" }, +] + +[[package]] +name = "speechbrain" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "hyperpyyaml" }, + { name = "joblib" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "scipy" }, + { name = "sentencepiece" }, + { name = "torch" }, + { name = "torchaudio" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/10/87e666544a4e0cec7cbdc09f26948994831ae0f8bbc58de3bf53b68285ff/speechbrain-1.0.3.tar.gz", hash = "sha256:fcab3c6e90012cecb1eed40ea235733b550137e73da6bfa2340ba191ec714052", size = 747735, upload-time = "2025-04-07T17:17:06.749Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/13/e61f1085aebee17d5fc2df19fcc5177c10379be52578afbecdd615a831c9/speechbrain-1.0.3-py3-none-any.whl", hash = "sha256:9859d4c1b1fb3af3b85523c0c89f52e45a04f305622ed55f31aa32dd2fba19e9", size = 864091, upload-time = "2025-04-07T17:17:04.706Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.45" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/f9/5e4491e5ccf42f5d9cfc663741d261b3e6e1683ae7812114e7636409fcc6/sqlalchemy-2.0.45.tar.gz", hash = "sha256:1632a4bda8d2d25703fdad6363058d882541bdaaee0e5e3ddfa0cd3229efce88", size = 9869912, upload-time = "2025-12-09T21:05:16.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/1c/769552a9d840065137272ebe86ffbb0bc92b0f1e0a68ee5266a225f8cd7b/sqlalchemy-2.0.45-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e90a344c644a4fa871eb01809c32096487928bd2038bf10f3e4515cb688cc56", size = 2153860, upload-time = "2025-12-10T20:03:23.843Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f8/9be54ff620e5b796ca7b44670ef58bc678095d51b0e89d6e3102ea468216/sqlalchemy-2.0.45-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8c8b41b97fba5f62349aa285654230296829672fc9939cd7f35aab246d1c08b", size = 3309379, upload-time = "2025-12-09T22:06:07.461Z" }, + { url = "https://files.pythonhosted.org/packages/f6/2b/60ce3ee7a5ae172bfcd419ce23259bb874d2cddd44f67c5df3760a1e22f9/sqlalchemy-2.0.45-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12c694ed6468333a090d2f60950e4250b928f457e4962389553d6ba5fe9951ac", size = 3309948, upload-time = "2025-12-09T22:09:57.643Z" }, + { url = "https://files.pythonhosted.org/packages/a3/42/bac8d393f5db550e4e466d03d16daaafd2bad1f74e48c12673fb499a7fc1/sqlalchemy-2.0.45-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f7d27a1d977a1cfef38a0e2e1ca86f09c4212666ce34e6ae542f3ed0a33bc606", size = 3261239, upload-time = "2025-12-09T22:06:08.879Z" }, + { url = "https://files.pythonhosted.org/packages/6f/12/43dc70a0528c59842b04ea1c1ed176f072a9b383190eb015384dd102fb19/sqlalchemy-2.0.45-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d62e47f5d8a50099b17e2bfc1b0c7d7ecd8ba6b46b1507b58cc4f05eefc3bb1c", size = 3284065, upload-time = "2025-12-09T22:09:59.454Z" }, + { url = "https://files.pythonhosted.org/packages/cf/9c/563049cf761d9a2ec7bc489f7879e9d94e7b590496bea5bbee9ed7b4cc32/sqlalchemy-2.0.45-cp311-cp311-win32.whl", hash = "sha256:3c5f76216e7b85770d5bb5130ddd11ee89f4d52b11783674a662c7dd57018177", size = 2113480, upload-time = "2025-12-09T21:29:57.03Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fa/09d0a11fe9f15c7fa5c7f0dd26be3d235b0c0cbf2f9544f43bc42efc8a24/sqlalchemy-2.0.45-cp311-cp311-win_amd64.whl", hash = "sha256:a15b98adb7f277316f2c276c090259129ee4afca783495e212048daf846654b2", size = 2138407, upload-time = "2025-12-09T21:29:58.556Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c7/1900b56ce19bff1c26f39a4ce427faec7716c81ac792bfac8b6a9f3dca93/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3ee2aac15169fb0d45822983631466d60b762085bc4535cd39e66bea362df5f", size = 3333760, upload-time = "2025-12-09T22:11:02.66Z" }, + { url = "https://files.pythonhosted.org/packages/0a/93/3be94d96bb442d0d9a60e55a6bb6e0958dd3457751c6f8502e56ef95fed0/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba547ac0b361ab4f1608afbc8432db669bd0819b3e12e29fb5fa9529a8bba81d", size = 3348268, upload-time = "2025-12-09T22:13:49.054Z" }, + { url = "https://files.pythonhosted.org/packages/48/4b/f88ded696e61513595e4a9778f9d3f2bf7332cce4eb0c7cedaabddd6687b/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215f0528b914e5c75ef2559f69dca86878a3beeb0c1be7279d77f18e8d180ed4", size = 3278144, upload-time = "2025-12-09T22:11:04.14Z" }, + { url = "https://files.pythonhosted.org/packages/ed/6a/310ecb5657221f3e1bd5288ed83aa554923fb5da48d760a9f7622afeb065/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:107029bf4f43d076d4011f1afb74f7c3e2ea029ec82eb23d8527d5e909e97aa6", size = 3313907, upload-time = "2025-12-09T22:13:50.598Z" }, + { url = "https://files.pythonhosted.org/packages/5c/39/69c0b4051079addd57c84a5bfb34920d87456dd4c90cf7ee0df6efafc8ff/sqlalchemy-2.0.45-cp312-cp312-win32.whl", hash = "sha256:0c9f6ada57b58420a2c0277ff853abe40b9e9449f8d7d231763c6bc30f5c4953", size = 2112182, upload-time = "2025-12-09T21:39:30.824Z" }, + { url = "https://files.pythonhosted.org/packages/f7/4e/510db49dd89fc3a6e994bee51848c94c48c4a00dc905e8d0133c251f41a7/sqlalchemy-2.0.45-cp312-cp312-win_amd64.whl", hash = "sha256:8defe5737c6d2179c7997242d6473587c3beb52e557f5ef0187277009f73e5e1", size = 2139200, upload-time = "2025-12-09T21:39:32.321Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e1/3ccb13c643399d22289c6a9786c1a91e3dcbb68bce4beb44926ac2c557bf/sqlalchemy-2.0.45-py3-none-any.whl", hash = "sha256:5225a288e4c8cc2308dbdd874edad6e7d0fd38eac1e9e5f23503425c8eee20d0", size = 1936672, upload-time = "2025-12-09T21:54:52.608Z" }, +] + +[[package]] +name = "sqlite-vec" +version = "0.1.9" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/85/9fad0045d8e7c8df3e0fa5a56c630e8e15ad6e5ca2e6106fceb666aa6638/sqlite_vec-0.1.9-py3-none-macosx_10_6_x86_64.whl", hash = "sha256:1b62a7f0a060d9475575d4e599bbf94a13d85af896bc1ce86ee80d1b5b48e5fb", size = 131171, upload-time = "2026-03-31T08:02:31.717Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3d/3677e0cd2f92e5ebc43cd29fbf565b75582bff1ccfa0b8327c7508e1084f/sqlite_vec-0.1.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d52e30513bae4cc9778ddbf6145610434081be4c3afe57cd877893bad9f6b6c", size = 165434, upload-time = "2026-03-31T08:02:32.712Z" }, + { url = "https://files.pythonhosted.org/packages/00/d4/f2b936d3bdc38eadcbd2a87875815db36430fab0363182ba5d12cd8e0b51/sqlite_vec-0.1.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e921e592f24a5f9a18f590b6ddd530eb637e2d474e3b1972f9bbeb773aa3cb9", size = 160076, upload-time = "2026-03-31T08:02:33.796Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ad/6afd073b0f817b3e03f9e37ad626ae341805891f23c74b5292818f49ac63/sqlite_vec-0.1.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux1_x86_64.whl", hash = "sha256:1515727990b49e79bcaf75fdee2ffc7d461f8b66905013231251f1c8938e7786", size = 163388, upload-time = "2026-03-31T08:02:34.888Z" }, + { url = "https://files.pythonhosted.org/packages/42/89/81b2907cda14e566b9bf215e2ad82fc9b349edf07d2010756ffdb902f328/sqlite_vec-0.1.9-py3-none-win_amd64.whl", hash = "sha256:4a28dc12fa4b53d7b1dced22da2488fade444e96b5d16fd2d698cd670675cf32", size = 292804, upload-time = "2026-03-31T08:02:36.035Z" }, +] + +[[package]] +name = "starlette" +version = "0.50.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, +] + +[[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 = "tabulate" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" }, +] + +[[package]] +name = "tensorboardx" +version = "2.6.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "packaging" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2b/c5/d4cc6e293fb837aaf9f76dd7745476aeba8ef7ef5146c3b3f9ee375fe7a5/tensorboardx-2.6.4.tar.gz", hash = "sha256:b163ccb7798b31100b9f5fa4d6bc22dad362d7065c2f24b51e50731adde86828", size = 4769801, upload-time = "2025-06-10T22:37:07.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/1d/b5d63f1a6b824282b57f7b581810d20b7a28ca951f2d5b59f1eb0782c12b/tensorboardx-2.6.4-py3-none-any.whl", hash = "sha256:5970cf3a1f0a6a6e8b180ccf46f3fe832b8a25a70b86e5a237048a7c0beb18e2", size = 87201, upload-time = "2025-06-10T22:37:05.44Z" }, +] + +[[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 = "tiktoken" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/46/21ea696b21f1d6d1efec8639c204bdf20fde8bafb351e1355c72c5d7de52/tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb", size = 1051565, upload-time = "2025-10-06T20:21:44.566Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d9/35c5d2d9e22bb2a5f74ba48266fb56c63d76ae6f66e02feb628671c0283e/tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa", size = 995284, upload-time = "2025-10-06T20:21:45.622Z" }, + { url = "https://files.pythonhosted.org/packages/01/84/961106c37b8e49b9fdcf33fe007bb3a8fdcc380c528b20cc7fbba80578b8/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc", size = 1129201, upload-time = "2025-10-06T20:21:47.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d0/3d9275198e067f8b65076a68894bb52fd253875f3644f0a321a720277b8a/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded", size = 1152444, upload-time = "2025-10-06T20:21:48.139Z" }, + { url = "https://files.pythonhosted.org/packages/78/db/a58e09687c1698a7c592e1038e01c206569b86a0377828d51635561f8ebf/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd", size = 1195080, upload-time = "2025-10-06T20:21:49.246Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/a9e4d2bf91d515c0f74afc526fd773a812232dd6cda33ebea7f531202325/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967", size = 1255240, upload-time = "2025-10-06T20:21:50.274Z" }, + { url = "https://files.pythonhosted.org/packages/9d/15/963819345f1b1fb0809070a79e9dd96938d4ca41297367d471733e79c76c/tiktoken-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e68e3e593637b53e56f7237be560f7a394451cb8c11079755e80ae64b9e6def", size = 879422, upload-time = "2025-10-06T20:21:51.734Z" }, + { url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728, upload-time = "2025-10-06T20:21:52.756Z" }, + { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049, upload-time = "2025-10-06T20:21:53.782Z" }, + { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008, upload-time = "2025-10-06T20:21:54.832Z" }, + { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665, upload-time = "2025-10-06T20:21:56.129Z" }, + { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230, upload-time = "2025-10-06T20:21:57.546Z" }, + { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688, upload-time = "2025-10-06T20:21:58.619Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd", size = 878694, upload-time = "2025-10-06T20:21:59.876Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.22.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, +] + +[[package]] +name = "tomlkit" +version = "0.13.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", size = 185207, upload-time = "2025-06-05T07:13:44.947Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901, upload-time = "2025-06-05T07:13:43.546Z" }, +] + +[[package]] +name = "torch" +version = "2.8.0+cu128" +source = { registry = "https://download.pytorch.org/whl/cu128" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx" }, + { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufile-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "setuptools", marker = "python_full_version >= '3.12'" }, + { name = "sympy" }, + { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.8.0%2Bcu128-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:039b9dcdd6bdbaa10a8a5cd6be22c4cb3e3589a341e5f904cbb571ca28f55bed", upload-time = "2025-10-01T23:49:06Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.8.0%2Bcu128-cp311-cp311-win_amd64.whl", hash = "sha256:34c55443aafd31046a7963b63d30bc3b628ee4a704f826796c865fdfd05bb596", upload-time = "2025-10-01T23:49:30Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.8.0%2Bcu128-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4354fc05bb79b208d6995a04ca1ceef6a9547b1c4334435574353d381c55087c", upload-time = "2025-10-01T23:51:02Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.8.0%2Bcu128-cp312-cp312-win_amd64.whl", hash = "sha256:0ad925202387f4e7314302a1b4f8860fa824357f9b1466d7992bf276370ebcff", upload-time = "2025-10-01T23:51:26Z" }, +] + +[[package]] +name = "torch-audiomentations" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "julius" }, + { name = "torch" }, + { name = "torch-pitch-shift" }, + { name = "torchaudio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/31/8d/2f8fd7e34c75f5ee8de4310c3bd3f22270acd44d1f809e2fe7c12fbf35f8/torch_audiomentations-0.12.0.tar.gz", hash = "sha256:b02d4c5eb86376986a53eb405cca5e34f370ea9284411237508e720c529f7888", size = 52094, upload-time = "2025-01-15T09:07:01.071Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/9d/1ee04f49c15d2d632f6f7102061d7c07652858e6d91b58a091531034e84f/torch_audiomentations-0.12.0-py3-none-any.whl", hash = "sha256:1b80b91d2016ccf83979622cac8f702072a79b7dcc4c2bee40f00b26433a786b", size = 48506, upload-time = "2025-01-15T09:06:59.687Z" }, +] + +[[package]] +name = "torch-pitch-shift" +version = "1.2.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "primepy" }, + { name = "torch" }, + { name = "torchaudio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/a6/722a832bca75d5079f6731e005b3d0c2eec7c6c6863d030620952d143d57/torch_pitch_shift-1.2.5.tar.gz", hash = "sha256:6e1c7531f08d0f407a4c55e5ff8385a41355c5c5d27ab7fa08632e51defbd0ed", size = 4725, upload-time = "2024-09-25T19:10:12.922Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/4c/96ac2a09efb56cc3c41fb3ce9b6f4d8c0604499f7481d4a13a7b03e21382/torch_pitch_shift-1.2.5-py3-none-any.whl", hash = "sha256:6f8500cbc13f1c98b11cde1805ce5084f82cdd195c285f34287541f168a7c6a7", size = 5005, upload-time = "2024-09-25T19:10:11.521Z" }, +] + +[[package]] +name = "torchaudio" +version = "2.8.0+cu128" +source = { registry = "https://download.pytorch.org/whl/cu128" } +dependencies = [ + { name = "torch" }, +] +wheels = [ + { url = "https://download-r2.pytorch.org/whl/cu128/torchaudio-2.8.0%2Bcu128-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:f4409df567d0723a7a3a89d32c7552a17e0ff6f137ea26a0d268c665259b2995", upload-time = "2025-08-05T20:12:07Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchaudio-2.8.0%2Bcu128-cp311-cp311-win_amd64.whl", hash = "sha256:7a1eb6154e05b8056b34c7a41495e09d57f79eb0180eb4e7f3bb2a61845ca8ea", upload-time = "2025-08-05T20:12:07Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchaudio-2.8.0%2Bcu128-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:145b8a0c21cfcaa1705c67173c5d439087e0e120d5da9bc344746f937901d243", upload-time = "2025-08-05T20:12:07Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchaudio-2.8.0%2Bcu128-cp312-cp312-win_amd64.whl", hash = "sha256:cce3a60cd9a97f7360c8f95504ac349311fb7d6b9b826135936764f4de5f782d", upload-time = "2025-08-05T20:12:07Z" }, +] + +[[package]] +name = "torchmetrics" +version = "1.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lightning-utilities" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "torch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/2e/48a887a59ecc4a10ce9e8b35b3e3c5cef29d902c4eac143378526e7485cb/torchmetrics-1.8.2.tar.gz", hash = "sha256:cf64a901036bf107f17a524009eea7781c9c5315d130713aeca5747a686fe7a5", size = 580679, upload-time = "2025-09-03T14:00:54.077Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/21/aa0f434434c48490f91b65962b1ce863fdcce63febc166ca9fe9d706c2b6/torchmetrics-1.8.2-py3-none-any.whl", hash = "sha256:08382fd96b923e39e904c4d570f3d49e2cc71ccabd2a94e0f895d1f0dac86242", size = 983161, upload-time = "2025-09-03T14:00:51.921Z" }, +] + +[[package]] +name = "torchvision" +version = "0.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pillow" }, + { name = "torch" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/d7/15d3d7bd8d0239211b21673d1bac7bc345a4ad904a8e25bb3fd8a9cf1fbc/torchvision-0.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:49aa20e21f0c2bd458c71d7b449776cbd5f16693dd5807195a820612b8a229b7", size = 1856884, upload-time = "2025-08-06T14:58:00.237Z" }, + { url = "https://files.pythonhosted.org/packages/dd/14/7b44fe766b7d11e064c539d92a172fa9689a53b69029e24f2f1f51e7dc56/torchvision-0.23.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:01dc33ee24c79148aee7cdbcf34ae8a3c9da1674a591e781577b716d233b1fa6", size = 2395543, upload-time = "2025-08-06T14:58:04.373Z" }, + { url = "https://files.pythonhosted.org/packages/79/9c/fcb09aff941c8147d9e6aa6c8f67412a05622b0c750bcf796be4c85a58d4/torchvision-0.23.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:35c27941831b653f5101edfe62c03d196c13f32139310519e8228f35eae0e96a", size = 8628388, upload-time = "2025-08-06T14:58:07.802Z" }, + { url = "https://files.pythonhosted.org/packages/93/40/3415d890eb357b25a8e0a215d32365a88ecc75a283f75c4e919024b22d97/torchvision-0.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:09bfde260e7963a15b80c9e442faa9f021c7e7f877ac0a36ca6561b367185013", size = 1600741, upload-time = "2025-08-06T14:57:59.158Z" }, + { url = "https://files.pythonhosted.org/packages/df/1d/0ea0b34bde92a86d42620f29baa6dcbb5c2fc85990316df5cb8f7abb8ea2/torchvision-0.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e0e2c04a91403e8dd3af9756c6a024a1d9c0ed9c0d592a8314ded8f4fe30d440", size = 1856885, upload-time = "2025-08-06T14:58:06.503Z" }, + { url = "https://files.pythonhosted.org/packages/e2/00/2f6454decc0cd67158c7890364e446aad4b91797087a57a78e72e1a8f8bc/torchvision-0.23.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:6dd7c4d329a0e03157803031bc856220c6155ef08c26d4f5bbac938acecf0948", size = 2396614, upload-time = "2025-08-06T14:58:03.116Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b5/3e580dcbc16f39a324f3dd71b90edbf02a42548ad44d2b4893cc92b1194b/torchvision-0.23.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4e7d31c43bc7cbecbb1a5652ac0106b436aa66e26437585fc2c4b2cf04d6014c", size = 8627108, upload-time = "2025-08-06T14:58:12.956Z" }, + { url = "https://files.pythonhosted.org/packages/82/c1/c2fe6d61e110a8d0de2f94276899a2324a8f1e6aee559eb6b4629ab27466/torchvision-0.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2e45272abe7b8bf0d06c405e78521b5757be1bd0ed7e5cd78120f7fdd4cbf35", size = 1600723, upload-time = "2025-08-06T14:57:57.986Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, +] + +[[package]] +name = "transformers" +version = "4.57.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "requests" }, + { name = "safetensors" }, + { name = "tokenizers" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/35/67252acc1b929dc88b6602e8c4a982e64f31e733b804c14bc24b47da35e6/transformers-4.57.6.tar.gz", hash = "sha256:55e44126ece9dc0a291521b7e5492b572e6ef2766338a610b9ab5afbb70689d3", size = 10134912, upload-time = "2026-01-16T10:38:39.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/b8/e484ef633af3887baeeb4b6ad12743363af7cce68ae51e938e00aaa0529d/transformers-4.57.6-py3-none-any.whl", hash = "sha256:4c9e9de11333ddfe5114bc872c9f370509198acf0b87a832a0ab9458e2bd0550", size = 11993498, upload-time = "2026-01-16T10:38:31.289Z" }, +] + +[[package]] +name = "triton" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "setuptools", marker = "platform_machine != 's390x'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/39/43325b3b651d50187e591eefa22e236b2981afcebaefd4f2fc0ea99df191/triton-3.4.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b70f5e6a41e52e48cfc087436c8a28c17ff98db369447bcaff3b887a3ab4467", size = 155531138, upload-time = "2025-07-30T19:58:29.908Z" }, + { url = "https://files.pythonhosted.org/packages/d0/66/b1eb52839f563623d185f0927eb3530ee4d5ffe9d377cdaf5346b306689e/triton-3.4.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31c1d84a5c0ec2c0f8e8a072d7fd150cab84a9c239eaddc6706c081bfae4eb04", size = 155560068, upload-time = "2025-07-30T19:58:37.081Z" }, +] + +[[package]] +name = "typer" +version = "0.21.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/bf/8825b5929afd84d0dabd606c67cd57b8388cb3ec385f7ef19c5cc2202069/typer-0.21.1.tar.gz", hash = "sha256:ea835607cd752343b6b2b7ce676893e5a0324082268b48f27aa058bdb7d2145d", size = 110371, upload-time = "2026-01-06T11:21:10.989Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01", size = 47381, upload-time = "2026-01-06T11:21:09.824Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[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 = "tzdata" +version = "2025.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, +] + +[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" }, +] + +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, + { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, + { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, + { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, + { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, + { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, + { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/70/1469ef1d3542ae7c2c7b72bd5e3a4e6ee69d7978fa8a3af05a38eca5becf/werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67", size = 864754, upload-time = "2026-01-08T17:49:23.247Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc", size = 225025, upload-time = "2026-01-08T17:49:21.859Z" }, +] + +[[package]] +name = "yarl" +version = "1.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/27/5ab13fc84c76a0250afd3d26d5936349a35be56ce5785447d6c423b26d92/yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", size = 141607, upload-time = "2025-10-06T14:09:16.298Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a1/d065d51d02dc02ce81501d476b9ed2229d9a990818332242a882d5d60340/yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", size = 94027, upload-time = "2025-10-06T14:09:17.786Z" }, + { url = "https://files.pythonhosted.org/packages/c1/da/8da9f6a53f67b5106ffe902c6fa0164e10398d4e150d85838b82f424072a/yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", size = 94963, upload-time = "2025-10-06T14:09:19.662Z" }, + { url = "https://files.pythonhosted.org/packages/68/fe/2c1f674960c376e29cb0bec1249b117d11738db92a6ccc4a530b972648db/yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d", size = 368406, upload-time = "2025-10-06T14:09:21.402Z" }, + { url = "https://files.pythonhosted.org/packages/95/26/812a540e1c3c6418fec60e9bbd38e871eaba9545e94fa5eff8f4a8e28e1e/yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503", size = 336581, upload-time = "2025-10-06T14:09:22.98Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f5/5777b19e26fdf98563985e481f8be3d8a39f8734147a6ebf459d0dab5a6b/yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65", size = 388924, upload-time = "2025-10-06T14:09:24.655Z" }, + { url = "https://files.pythonhosted.org/packages/86/08/24bd2477bd59c0bbd994fe1d93b126e0472e4e3df5a96a277b0a55309e89/yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e", size = 392890, upload-time = "2025-10-06T14:09:26.617Z" }, + { url = "https://files.pythonhosted.org/packages/46/00/71b90ed48e895667ecfb1eaab27c1523ee2fa217433ed77a73b13205ca4b/yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d", size = 365819, upload-time = "2025-10-06T14:09:28.544Z" }, + { url = "https://files.pythonhosted.org/packages/30/2d/f715501cae832651d3282387c6a9236cd26bd00d0ff1e404b3dc52447884/yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7", size = 363601, upload-time = "2025-10-06T14:09:30.568Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f9/a678c992d78e394e7126ee0b0e4e71bd2775e4334d00a9278c06a6cce96a/yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967", size = 358072, upload-time = "2025-10-06T14:09:32.528Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d1/b49454411a60edb6fefdcad4f8e6dbba7d8019e3a508a1c5836cba6d0781/yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed", size = 385311, upload-time = "2025-10-06T14:09:34.634Z" }, + { url = "https://files.pythonhosted.org/packages/87/e5/40d7a94debb8448c7771a916d1861d6609dddf7958dc381117e7ba36d9e8/yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6", size = 381094, upload-time = "2025-10-06T14:09:36.268Z" }, + { url = "https://files.pythonhosted.org/packages/35/d8/611cc282502381ad855448643e1ad0538957fc82ae83dfe7762c14069e14/yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e", size = 370944, upload-time = "2025-10-06T14:09:37.872Z" }, + { url = "https://files.pythonhosted.org/packages/2d/df/fadd00fb1c90e1a5a8bd731fa3d3de2e165e5a3666a095b04e31b04d9cb6/yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca", size = 81804, upload-time = "2025-10-06T14:09:39.359Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f7/149bb6f45f267cb5c074ac40c01c6b3ea6d8a620d34b337f6321928a1b4d/yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b", size = 86858, upload-time = "2025-10-06T14:09:41.068Z" }, + { url = "https://files.pythonhosted.org/packages/2b/13/88b78b93ad3f2f0b78e13bfaaa24d11cbc746e93fe76d8c06bf139615646/yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376", size = 81637, upload-time = "2025-10-06T14:09:42.712Z" }, + { url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" }, + { url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940, upload-time = "2025-10-06T14:09:50.089Z" }, + { url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825, upload-time = "2025-10-06T14:09:52.142Z" }, + { url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705, upload-time = "2025-10-06T14:09:54.128Z" }, + { url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518, upload-time = "2025-10-06T14:09:55.762Z" }, + { url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267, upload-time = "2025-10-06T14:09:57.958Z" }, + { url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797, upload-time = "2025-10-06T14:09:59.527Z" }, + { url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535, upload-time = "2025-10-06T14:10:01.139Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" }, + { url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" }, + { url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" }, + { url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" }, + { url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" }, + { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, +] + +[[package]] +name = "youtube-transcript-api" +version = "1.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "defusedxml" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/43/4104185a2eaa839daa693b30e15c37e7e58795e8e09ec414f22b3db54bec/youtube_transcript_api-1.2.4.tar.gz", hash = "sha256:b72d0e96a335df599d67cee51d49e143cff4f45b84bcafc202ff51291603ddcd", size = 469839, upload-time = "2026-01-29T09:09:17.088Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/95/129ea37efd6cd6ed00f62baae6543345c677810b8a3bf0026756e1d3cf3c/youtube_transcript_api-1.2.4-py3-none-any.whl", hash = "sha256:03878759356da5caf5edac77431780b91448fb3d8c21d4496015bdc8a7bc43ff", size = 485227, upload-time = "2026-01-29T09:09:15.427Z" }, +] + +[[package]] +name = "yt-dlp" +version = "2026.7.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/c5/9972af4b472b0d55badf841ebafd2f98944cb0ae0f46e11d01f363ea5b91/yt_dlp-2026.7.4.tar.gz", hash = "sha256:b094813404f87a9dd2186f00815231df32e5fd8a5403be0f807b3bb2d21a4432", size = 3049326, upload-time = "2026-07-04T22:42:14.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/8a/cd4c9b02c10c563adfe78118310129641900e1cd6de888cfae2452072696/yt_dlp-2026.7.4-py3-none-any.whl", hash = "sha256:f11f2b11d5a8ac4059f9bdf29fa4407dc7c6bb00c5097e95ca22a7a9db518266", size = 3184705, upload-time = "2026-07-04T22:42:12.989Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/21/093488dfc7cc8964ded15ab726fad40f25fd3d788fd741cc1c5a17d78ee8/zipp-3.23.1.tar.gz", hash = "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110", size = 25965, upload-time = "2026-04-13T23:21:46.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/8a/0861bec20485572fbddf3dfba2910e38fe249796cb73ecdeb74e07eeb8d3/zipp-3.23.1-py3-none-any.whl", hash = "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc", size = 10378, upload-time = "2026-04-13T23:21:45.386Z" }, +] From 707b2162914b826d9a9643299ea3cdf3df2e2062 Mon Sep 17 00:00:00 2001 From: cmander02 <15946840899@163.com> Date: Tue, 28 Jul 2026 11:17:46 +0800 Subject: [PATCH 02/25] feat: route desktop tasks to remote workers --- .../components/folder-queue-dialog.test.tsx | 115 ++++++++++++ web/src/components/folder-queue-dialog.tsx | 49 +++-- web/src/components/pages/submit-page.test.tsx | 172 +++++++++++++++++- web/src/components/pages/submit-page.tsx | 116 +++++++++++- web/src/components/settings-panel.tsx | 104 +++++++++++ .../components/settings/model-sections.tsx | 10 +- web/src/components/task-card.tsx | 5 + web/src/components/task-detail.tsx | 22 ++- web/src/lib/api.ts | 40 +++- web/src/lib/settings-schema.ts | 11 ++ web/src/lib/task-routing.test.ts | 74 ++++++++ web/src/lib/task-routing.ts | 70 +++++++ 12 files changed, 754 insertions(+), 34 deletions(-) create mode 100644 web/src/components/folder-queue-dialog.test.tsx create mode 100644 web/src/lib/task-routing.test.ts create mode 100644 web/src/lib/task-routing.ts diff --git a/web/src/components/folder-queue-dialog.test.tsx b/web/src/components/folder-queue-dialog.test.tsx new file mode 100644 index 0000000..051f046 --- /dev/null +++ b/web/src/components/folder-queue-dialog.test.tsx @@ -0,0 +1,115 @@ +/** @vitest-environment jsdom */ + +import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react" +import "@testing-library/jest-dom/vitest" +import { afterEach, describe, expect, it, vi } from "vitest" + +import { FolderQueueDialog } from "./folder-queue-dialog" +import { api } from "@/lib/api" +import { PREFERRED_WORKER_OPTION } from "@/lib/task-routing" + +afterEach(() => { + cleanup() + vi.restoreAllMocks() +}) + +function mockFolderBrowser() { + vi.spyOn(api.filesystem, "drives").mockResolvedValue({ + success: true, + drives: [{ name: "C:", path: "C:\\", is_dir: true, size: null }], + }) + vi.spyOn(api.filesystem, "browse").mockResolvedValue({ + success: true, + path: "C:\\", + items: [{ + name: "Media", + path: "C:\\Media", + is_dir: true, + size: null, + }], + }) + vi.spyOn(api.filesystem, "scanFolder").mockResolvedValue({ + success: true, + files: [ + { path: "C:\\Media\\one.mp4", name: "one.mp4", size: 1024 }, + { path: "C:\\Media\\two.mp3", name: "two.mp3", size: 2048 }, + ], + count: 2, + }) +} + +async function selectMediaFolder() { + const folder = await screen.findByRole("button", { name: /Media/ }) + fireEvent.click(folder) + await screen.findByText("one.mp4") +} + +describe("FolderQueueDialog processing target guard", () => { + it("shows a clear warning and blocks server submission for an EXE-local folder", async () => { + mockFolderBrowser() + const create = vi.spyOn(api.tasks, "create").mockResolvedValue({} as never) + + render( + , + ) + await selectMediaFolder() + + expect(screen.getByRole("alert")).toHaveTextContent( + "服务器无法访问此 EXE 上的文件夹", + ) + expect(screen.getByRole("button", { name: "提交全部 2 个文件" })).toBeDisabled() + expect(create).not.toHaveBeenCalled() + }) + + it("submits every folder entry to the selected concrete worker", async () => { + mockFolderBrowser() + const create = vi.spyOn(api.tasks, "create").mockResolvedValue({} as never) + const onSubmitted = vi.fn() + + render( + , + ) + await selectMediaFolder() + fireEvent.click(screen.getByRole("button", { name: "提交全部 2 个文件" })) + + await waitFor(() => expect(create).toHaveBeenCalledTimes(2)) + expect(create).toHaveBeenNthCalledWith( + 1, + "C:\\Media\\one.mp4", + { + force_asr: true, + [PREFERRED_WORKER_OPTION]: "desktop-a", + }, + { + origin_client: "web", + requested_executor: "exe", + }, + ) + expect(create).toHaveBeenNthCalledWith( + 2, + "C:\\Media\\two.mp3", + { + force_asr: true, + [PREFERRED_WORKER_OPTION]: "desktop-a", + }, + { + origin_client: "web", + requested_executor: "exe", + }, + ) + expect(onSubmitted).toHaveBeenCalledOnce() + }) +}) diff --git a/web/src/components/folder-queue-dialog.tsx b/web/src/components/folder-queue-dialog.tsx index 1d3ca5d..d6396c4 100644 --- a/web/src/components/folder-queue-dialog.tsx +++ b/web/src/components/folder-queue-dialog.tsx @@ -14,6 +14,12 @@ import { import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { api } from "@/lib/api" +import { + isServerProcessingTarget, + requestedExecutorForTarget, + withProcessingTargetOptions, + type ProcessingTarget, +} from "@/lib/task-routing" import { HugeiconsIcon } from "@hugeicons/react" import { ArrowRight01Icon, @@ -40,11 +46,6 @@ interface MediaFile { size: number } -const MEDIA_EXTS = new Set([ - "mp4", "mkv", "avi", "webm", "mov", "flv", "wmv", - "mp3", "wav", "aac", "flac", "ogg", "m4a", "wma", -]) - function isVideo(name: string) { const ext = name.split(".").pop()?.toLowerCase() ?? "" return ["mp4", "mkv", "avi", "webm", "mov", "flv", "wmv"].includes(ext) @@ -60,11 +61,19 @@ interface Props { open: boolean onOpenChange: (open: boolean) => void options: Record + processingTarget?: ProcessingTarget + serverLocalPathsBlocked?: boolean onSubmitted?: () => void } -export function FolderQueueDialog({ open, onOpenChange, options, onSubmitted }: Props) { - const [currentPath, setCurrentPath] = useState("") +export function FolderQueueDialog({ + open, + onOpenChange, + options, + processingTarget = "server", + serverLocalPathsBlocked = false, + onSubmitted, +}: Props) { const [items, setItems] = useState([]) const [drives, setDrives] = useState([]) const [loading, setLoading] = useState(false) @@ -105,7 +114,6 @@ export function FolderQueueDialog({ open, onOpenChange, options, onSubmitted }: try { const data = await api.filesystem.browse(path, "directory") if (data.success) { - setCurrentPath(data.path) setPathInput(data.path) setItems(data.items ?? []) } @@ -131,17 +139,25 @@ export function FolderQueueDialog({ open, onOpenChange, options, onSubmitted }: } const handleSubmitAll = async () => { - if (!mediaFiles.length) return + if ( + !mediaFiles.length + || (serverLocalPathsBlocked && isServerProcessingTarget(processingTarget)) + ) return setSubmitting(true) setSubmitProgress(0) setFailedFiles([]) cancelledRef.current = false let count = 0 const failed: string[] = [] + const taskOptions = withProcessingTargetOptions(options, processingTarget) + const requestedExecutor = requestedExecutorForTarget(processingTarget) for (const file of mediaFiles) { if (cancelledRef.current) break try { - await api.tasks.create(file.path, options) + await api.tasks.create(file.path, taskOptions, { + origin_client: "web", + requested_executor: requestedExecutor, + }) } catch { failed.push(file.name) } @@ -294,6 +310,11 @@ export function FolderQueueDialog({ open, onOpenChange, options, onSubmitted }: ))} )} + {serverLocalPathsBlocked && isServerProcessingTarget(processingTarget) && ( +

+ 服务器无法访问此 EXE 上的文件夹。请选择 EXE 或具体 Worker 处理,URL 任务仍可由服务器处理。 +

+ )} )} @@ -312,7 +333,13 @@ export function FolderQueueDialog({ open, onOpenChange, options, onSubmitted }: ) : ( - { setSource(e.target.value) setCollection(null) + setError("") }} placeholder="粘贴视频链接或本地路径..." className="pl-9" @@ -345,6 +400,47 @@ export function SubmitPage() { +
+ + +

+ {localFileTargetBlocked + ? "服务器无法访问此 EXE 上的本地文件或文件夹。请选择 EXE 或具体 Worker 处理,URL 任务仍可由服务器处理。" + : processingTarget === "server" + ? "任务由服务器处理。" + : selectedWorker + ? selectedWorker.online + ? `任务将由 ${selectedWorker.name || selectedWorker.id} 处理。` + : `任务将等待 ${selectedWorker.name || selectedWorker.id} 上线后处理。` + : exeOnline + ? "EXE 已在线,将自动领取任务。" + : "任务保存在服务器,等待 EXE 上线后自动领取。"} +

+
+ {/* Submit */} - {segment.caption ? ( -
- ( - {children} - ), - }} - > - {segment.caption} - -
- ) : null} - + + {segment.content} + ) - }) - })()} + } + return ( +
+ + {segment.caption ? ( +
+ ( + {children} + ), + }} + > + {segment.caption} + +
+ ) : null} +
+ ) + })} (null) const [completedSteps, setCompletedSteps] = useState([]) - const [currentStep, setCurrentStep] = useState(null) const [taskError, setTaskError] = useState(null) const [taskFlow, setTaskFlow] = useState(null) const [timelineEvents, setTimelineEvents] = useState([]) @@ -531,7 +534,6 @@ export function ResultPageComplete({ archivePath, taskId: taskIdProp }: Props) { setActiveImageIdx(0) setTaskStatus(null) setCompletedSteps([]) - setCurrentStep(null) setTaskError(null) setTaskFlow(null) setTimelineEvents([]) @@ -580,7 +582,7 @@ export function ResultPageComplete({ archivePath, taskId: taskIdProp }: Props) { console.warn("Failed to save SRT after speaker rename:", err) } }, - [subtitles, archivePath, sep], + [subtitles, archivePath, sep, isPolished], ) const handleRenameSpeaker = async (oldName: string, newName: string) => { @@ -653,7 +655,6 @@ export function ResultPageComplete({ archivePath, taskId: taskIdProp }: Props) { const applyTaskSnapshot = useCallback((task: Task) => { setTaskStatus(task.status) - setCurrentStep(task.current_step) setCompletedSteps(task.completed_steps ?? []) setTaskError(task.error) setTaskFlow(task.flow ?? null) @@ -844,7 +845,9 @@ export function ResultPageComplete({ archivePath, taskId: taskIdProp }: Props) { if (descs && descs.length > 0) { resultDescriptions.push(...descs) } - } catch {} + } catch { + // Continue with descriptions discovered from the archive directory. + } } const resultByIndex = new Map(resultDescriptions.map((item) => [item.index, item])) const descs: ImageDescription[] = [] @@ -858,7 +861,11 @@ export function ResultPageComplete({ archivePath, taskId: taskIdProp }: Props) { const stem = item.name.replace(/\.[^.]+$/, "") const descPath = archivePath + sep + "descriptions" + sep + `${stem}.md` let text = "" - try { text = (await api.filesystem.read(descPath)).content ?? "" } catch {} + try { + text = (await api.filesystem.read(descPath)).content ?? "" + } catch { + // Description text is optional for an archived image. + } const index = Number.parseInt(stem, 10) const fromResult = Number.isFinite(index) ? resultByIndex.get(index) : undefined descs.push({ @@ -869,7 +876,9 @@ export function ResultPageComplete({ archivePath, taskId: taskIdProp }: Props) { text: (fromResult?.text || text || ""), }) } - } catch {} + } catch { + // Fall back to probing the legacy numbered image layout below. + } for (let i = descs.length > 0 ? 30 : 0; i < 30; i++) { const imgPath = archivePath + sep + "images" + sep + `${String(i).padStart(2, "0")}.jpg` const descPath = archivePath + sep + "descriptions" + sep + `${String(i).padStart(2, "0")}.md` @@ -877,7 +886,11 @@ export function ResultPageComplete({ archivePath, taskId: taskIdProp }: Props) { const check = await api.filesystem.read(imgPath) if (!check || !check.success) break let text = "" - try { text = (await api.filesystem.read(descPath)).content ?? "" } catch {} + try { + text = (await api.filesystem.read(descPath)).content ?? "" + } catch { + // Description text is optional for a legacy archived image. + } const fromResult = resultByIndex.get(i) descs.push({ ...(fromResult ?? {}), @@ -912,14 +925,12 @@ export function ResultPageComplete({ archivePath, taskId: taskIdProp }: Props) { // when the user navigates back to the result page mid-processing. onSnapshot(data) { setTaskStatus(data.status) - setCurrentStep(data.current_step) setCompletedSteps(data.completed_steps ?? []) if (data.flow) setTaskFlow(data.flow) if (data.error) setTaskError(data.error) }, onStep(data: StepEvent) { setTaskStatus("processing") - setCurrentStep(data.step) if (data.completed && !completedSteps.includes(data.step)) { setCompletedSteps((prev) => prev.includes(data.step) ? prev : [...prev, data.step], @@ -1302,7 +1313,6 @@ export function ResultPageComplete({ archivePath, taskId: taskIdProp }: Props) { ) : isImageNote ? (
- { setActiveTab(String(v)); updateActiveTab(String(v)) }} className="flex flex-col flex-1 min-h-0"> + { + setActiveTab(value) + updateActiveTab(value) + }} + className="flex flex-col flex-1 min-h-0" + >
@@ -1791,7 +1808,6 @@ export function ResultPageComplete({ archivePath, taskId: taskIdProp }: Props) { ) : isImageNote ? (
- { setActiveTab(String(v)); updateActiveTab(String(v)) }} className="flex flex-col flex-1 min-h-0"> + { + setActiveTab(value) + updateActiveTab(value) + }} + className="flex flex-col flex-1 min-h-0" + >
diff --git a/web/src/components/pages/result-page-live.tsx b/web/src/components/pages/result-page-live.tsx index 9255b45..8d3ff23 100644 --- a/web/src/components/pages/result-page-live.tsx +++ b/web/src/components/pages/result-page-live.tsx @@ -100,7 +100,9 @@ export function ResultPageLive({ taskId }: { taskId: string }) { if (t.status === "completed" || t.status === "failed" || t.status === "cancelled" || t.status === "paused") { clearInterval(interval) } - } catch {} + } catch { + // Keep the latest SSE snapshot when the fallback request is unavailable. + } }, 10000) return () => { @@ -130,6 +132,7 @@ export function ResultPageLive({ taskId }: { taskId: string }) { try { await api.tasks.cancel(taskId) } catch { + // The SSE stream will reconcile the task state after a transient failure. } finally { setActionBusy(null) } @@ -143,6 +146,7 @@ export function ResultPageLive({ taskId }: { taskId: string }) { setTask(next) setFlow(next.flow ?? null) } catch { + // The SSE stream will reconcile the task state after a transient failure. } finally { setActionBusy(null) } @@ -156,6 +160,7 @@ export function ResultPageLive({ taskId }: { taskId: string }) { setTask(next) setFlow(next.flow ?? null) } catch { + // The SSE stream will reconcile the task state after a transient failure. } finally { setActionBusy(null) } @@ -168,6 +173,7 @@ export function ResultPageLive({ taskId }: { taskId: string }) { setDeleteOpen(false) navigate("#/files") } catch { + // Stay on the live task when deletion fails. } finally { setActionBusy(null) } diff --git a/web/src/components/pages/result-page-wrapper.tsx b/web/src/components/pages/result-page-wrapper.tsx index 587850c..9e44bbc 100644 --- a/web/src/components/pages/result-page-wrapper.tsx +++ b/web/src/components/pages/result-page-wrapper.tsx @@ -4,7 +4,6 @@ */ import { useEffect, useState } from "react" import { useRoute } from "@/lib/router" -import { navigate } from "@/lib/router" import { api } from "@/lib/api" import { ResultPageLive } from "./result-page-live" import { ResultPageComplete } from "./result-page-complete" diff --git a/web/src/components/result/image-lightbox.tsx b/web/src/components/result/image-lightbox.tsx index bcdae43..8764118 100644 --- a/web/src/components/result/image-lightbox.tsx +++ b/web/src/components/result/image-lightbox.tsx @@ -30,11 +30,33 @@ export function ImageLightbox({ onIndexChange, onOpenChange, }: ImageLightboxProps) { + const current = images[index] + + if (!open || !current) return null + + return ( + + ) +} + +function ImageLightboxContent({ + images, + index, + current, + onIndexChange, + onOpenChange, +}: Omit & { current: LightboxImage }) { const [scale, setScale] = useState(1) const [offset, setOffset] = useState({ x: 0, y: 0 }) const [dragging, setDragging] = useState(false) const dragRef = useRef({ x: 0, y: 0, offsetX: 0, offsetY: 0 }) - const current = images[index] const resetView = useCallback(() => { setScale(1) @@ -50,7 +72,6 @@ export function ImageLightbox({ }, [images.length, index, onIndexChange, resetView]) useEffect(() => { - if (!open) return const onKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") onOpenChange(false) if (event.key === "ArrowLeft") go(-1) @@ -58,13 +79,7 @@ export function ImageLightbox({ } window.addEventListener("keydown", onKeyDown) return () => window.removeEventListener("keydown", onKeyDown) - }, [go, onOpenChange, open]) - - useEffect(() => { - if (open) resetView() - }, [index, open, resetView]) - - if (!open || !current) return null + }, [go, onOpenChange]) return (
void + activeIndex: number + onImageIndexChange: (index: number) => void isProcessing?: boolean } -export function ImageNoteViewer({ archivePath, descriptions, activeIndex, onImageIndexChange, isProcessing }: Props) { - const [currentIdx, setCurrentIdx] = useState(0) - const [imgError, setImgError] = useState(false) +export function ImageNoteViewer({ descriptions, activeIndex, onImageIndexChange, isProcessing }: Props) { const [lightboxOpen, setLightboxOpen] = useState(false) const total = descriptions.length + const matchingIndex = descriptions.findIndex((item) => item.index === activeIndex) + const currentIdx = matchingIndex >= 0 + ? matchingIndex + : Math.max(0, Math.min(total - 1, activeIndex)) const current = descriptions[currentIdx] const setImageIndex = useCallback((nextIndex: number) => { const next = Math.max(0, Math.min(total - 1, nextIndex)) - setCurrentIdx(next) - onImageIndexChange?.(descriptions[next]?.index ?? next) - setImgError(false) + onImageIndexChange(descriptions[next]?.index ?? next) }, [descriptions, total, onImageIndexChange]) const go = useCallback((delta: number) => { setImageIndex(currentIdx + delta) }, [currentIdx, setImageIndex]) - useEffect(() => { - setImgError(false) - }, [currentIdx]) - - useEffect(() => { - if (typeof activeIndex !== "number" || total <= 0) return - const byDescriptionIndex = descriptions.findIndex((item) => item.index === activeIndex) - const next = byDescriptionIndex >= 0 ? byDescriptionIndex : activeIndex - if (next >= 0 && next < total && next !== currentIdx) { - setCurrentIdx(next) - setImgError(false) - } - }, [activeIndex, currentIdx, descriptions, total]) - if (isProcessing && total === 0) { return (
@@ -82,20 +67,13 @@ export function ImageNoteViewer({ archivePath, descriptions, activeIndex, onImag
{/* Image display */}
- {imgUrl && !imgError ? ( - + {imgUrl ? ( + setLightboxOpen(true)} + /> ) : (
图片加载失败
)} @@ -135,3 +113,27 @@ export function ImageNoteViewer({ archivePath, descriptions, activeIndex, onImag
) } + +function NoteImage({ src, alt, onOpen }: { src: string; alt: string; onOpen: () => void }) { + const [loadFailed, setLoadFailed] = useState(false) + + if (loadFailed) { + return
图片加载失败
+ } + + return ( + + ) +} diff --git a/web/src/components/result/media-player.tsx b/web/src/components/result/media-player.tsx index fb07f84..b095755 100644 --- a/web/src/components/result/media-player.tsx +++ b/web/src/components/result/media-player.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useRef, type RefCallback } from "react" -import Artplayer from "artplayer" +import Artplayer, { type Option, type SettingOption } from "artplayer" import { srtToVTT } from "@/lib/srt" interface MediaPlayerProps { @@ -53,7 +53,7 @@ function VideoPlayer({ useEffect(() => { if (!containerRef.current) return - const options: Record = { + const options: Option = { container: containerRef.current, url: src, volume: 1, @@ -80,7 +80,7 @@ function VideoPlayer({ html: "循环", tooltip: loop ? "开" : "关", switch: loop, - onSwitch(item: any) { + onSwitch(item: SettingOption) { const nextState = !item.switch if (art.video) art.video.loop = nextState onLoopChange?.(nextState) @@ -91,12 +91,12 @@ function VideoPlayer({ } if (vttUrl) { - options.subtitle = { url: vttUrl, type: "vtt", style: { "font-size": "18px" }, encoding: "utf-8" } - options.settings = [...options.settings, { + options.subtitle = { url: vttUrl, type: "vtt", style: { fontSize: "18px" }, encoding: "utf-8" } + options.settings = [...(options.settings ?? []), { html: "字幕", tooltip: "开", switch: true, - onSwitch(item: any) { + onSwitch(item: SettingOption) { const nextState = !item.switch art.subtitle.show = nextState item.tooltip = nextState ? "开" : "关" @@ -105,7 +105,7 @@ function VideoPlayer({ }] } - const art = new Artplayer(options as any) + const art = new Artplayer(options) artRef.current = art diff --git a/web/src/components/result/speaker-panel.tsx b/web/src/components/result/speaker-panel.tsx index 0a5ac25..dfcdb3d 100644 --- a/web/src/components/result/speaker-panel.tsx +++ b/web/src/components/result/speaker-panel.tsx @@ -1,4 +1,4 @@ -import { Fragment, useMemo, useState, useRef, useEffect, type KeyboardEvent } from "react" +import { Fragment, useMemo, useState, useRef, type KeyboardEvent } from "react" import type { Subtitle } from "@/lib/srt" import { getSpeakerColor, extractSpeakers, formatSpeakerLabel } from "@/lib/srt" import { formatDuration } from "@/lib/format" @@ -25,15 +25,14 @@ export function SpeakerPanel({ subtitles, duration, currentTime, onSeek, onRenam const [editValue, setEditValue] = useState("") const inputRef = useRef(null) - useEffect(() => { - if (editingSpeaker) { - setEditValue(editingSpeaker) - setTimeout(() => { - inputRef.current?.focus() - inputRef.current?.select() - }, 30) - } - }, [editingSpeaker]) + const startEditing = (speaker: string) => { + setEditValue(speaker) + setEditingSpeaker(speaker) + setTimeout(() => { + inputRef.current?.focus() + inputRef.current?.select() + }, 30) + } const speakers = useMemo(() => { const names = extractSpeakers(subtitles) @@ -109,7 +108,7 @@ export function SpeakerPanel({ subtitles, duration, currentTime, onSeek, onRenam + )} + {status.phase === "APP_READY" && ( + + )} + + +
+ + {actionError && ( +

+ {actionError} +

+ )} +
+ + {diagnosticsOpen && ( +
+
+

启动诊断

+ {diagnostics && ( + + {diagnostics.logs.length} 条日志 + + )} +
+ + {diagnosticsLoading && ( +
+ + 正在读取诊断数据 +
+ )} + {diagnosticsError && ( +

+ {diagnosticsError} +

+ )} + {diagnostics && !diagnosticsLoading && ( +
+
+
当前阶段
+
{diagnostics.status.phase}
+
进程
+
{diagnostics.status.pid ?? "—"}
+
状态说明
+
{diagnostics.status.message || "—"}
+
+
+ {diagnostics.logs.length === 0 ? ( +

暂无启动日志。

+ ) : ( + diagnostics.logs.slice(-80).map((entry, index) => ( +
+ {entry.source} + {entry.line} +
+ )) + )} +
+
+ )} +
+ )} + + +

+ 启动页只通过桌面进程通信读取状态;服务健康后才会加载工作区数据。 +

+
+ + ) +} diff --git a/web/src/lib/bootstrap-entry.ts b/web/src/lib/bootstrap-entry.ts new file mode 100644 index 0000000..642c953 --- /dev/null +++ b/web/src/lib/bootstrap-entry.ts @@ -0,0 +1,3 @@ +export function isBootstrapEntry(search = window.location.search) { + return new URLSearchParams(search).get("bootstrap") === "1" +} diff --git a/web/src/lib/tauri.test.ts b/web/src/lib/tauri.test.ts new file mode 100644 index 0000000..6a22ca6 --- /dev/null +++ b/web/src/lib/tauri.test.ts @@ -0,0 +1,51 @@ +/** @vitest-environment jsdom */ + +import { describe, expect, it } from "vitest" +import { normalizeBootstrapStatus, type BackendStatus } from "@/lib/tauri" + +function backendStatus(overrides: Partial = {}): BackendStatus { + return { + state: "stopped", + command: "mpp-runtime run", + cwd: "backend", + pid: null, + url: "http://localhost:18000", + message: "Ready.", + ...overrides, + } +} + +describe("normalizeBootstrapStatus", () => { + it("preserves native bootstrap fields", () => { + const status = normalizeBootstrapStatus(backendStatus({ + state: "error", + phase: "FAILED_MANUAL", + error_code: "RUNTIME_INVALID", + component_id: "desktop-runtime", + remediation: "Repair the installation.", + local_path: "C:\\Program Files\\MPP\\runtime", + })) + + expect(status).toMatchObject({ + phase: "FAILED_MANUAL", + error_code: "RUNTIME_INVALID", + component_id: "desktop-runtime", + remediation: "Repair the installation.", + local_path: "C:\\Program Files\\MPP\\runtime", + }) + }) + + it("maps legacy backend states to stable bootstrap phases", () => { + expect(normalizeBootstrapStatus(backendStatus()).phase).toBe("READY_TO_START") + expect(normalizeBootstrapStatus(backendStatus({ + state: "starting", + message: "Waiting for the health endpoint.", + })).phase).toBe("WAITING_HEALTH") + expect(normalizeBootstrapStatus(backendStatus({ + state: "starting", + message: "Spawning process.", + })).phase).toBe("STARTING_BACKEND") + expect(normalizeBootstrapStatus(backendStatus({ state: "running" })).phase).toBe("APP_READY") + expect(normalizeBootstrapStatus(backendStatus({ state: "error" })).phase).toBe("FAILED_RETRYABLE") + }) +}) diff --git a/web/src/lib/tauri.ts b/web/src/lib/tauri.ts index b3aaa9c..d2e9dcb 100644 --- a/web/src/lib/tauri.ts +++ b/web/src/lib/tauri.ts @@ -3,6 +3,18 @@ import { open } from "@tauri-apps/plugin-dialog" export type BackendState = "stopped" | "starting" | "running" | "stopping" | "external" | "error" +export const BOOTSTRAP_PHASES = [ + "SCANNING", + "READY_TO_START", + "STARTING_BACKEND", + "WAITING_HEALTH", + "APP_READY", + "FAILED_RETRYABLE", + "FAILED_MANUAL", +] as const + +export type BootstrapPhase = (typeof BOOTSTRAP_PHASES)[number] + export interface BackendStatus { state: BackendState command: string @@ -10,6 +22,11 @@ export interface BackendStatus { pid: number | null url: string message: string + phase?: BootstrapPhase + error_code?: string | null + component_id?: string | null + remediation?: string | null + local_path?: string | null } export interface BackendLogEntry { @@ -28,7 +45,29 @@ export interface MppBackendBridge { onLog(callback: (entry: BackendLogEntry) => void): () => void } +export interface BootstrapStatus extends BackendStatus { + phase: BootstrapPhase + error_code: string | null + component_id: string | null + remediation: string | null + local_path: string | null +} + +export interface BootstrapDiagnostics { + status: BootstrapStatus + logs: BackendLogEntry[] +} + +export interface BootstrapBridge { + getStatus(): Promise + retry(): Promise + openLogs(): Promise + getDiagnostics(): Promise + onStatus(callback: (status: BootstrapStatus) => void): () => void +} + let tauriBackendBridge: MppBackendBridge | undefined +let tauriBootstrapBridge: BootstrapBridge | undefined type TauriWindow = Window & { __TAURI_INTERNALS__?: { @@ -67,6 +106,33 @@ function tauriInvoke(command: string, args?: Record) { return invoke(command, args) } +function isBootstrapPhase(value: unknown): value is BootstrapPhase { + return typeof value === "string" && BOOTSTRAP_PHASES.some((phase) => phase === value) +} + +function fallbackBootstrapPhase(status: BackendStatus): BootstrapPhase { + if (status.state === "running" || status.state === "external") return "APP_READY" + if (status.state === "starting") { + return /\b(health|healthy|ready|wait)\b/i.test(status.message) + ? "WAITING_HEALTH" + : "STARTING_BACKEND" + } + if (status.state === "error") return "FAILED_RETRYABLE" + if (status.state === "stopping") return "SCANNING" + return "READY_TO_START" +} + +export function normalizeBootstrapStatus(status: BackendStatus): BootstrapStatus { + return { + ...status, + phase: isBootstrapPhase(status.phase) ? status.phase : fallbackBootstrapPhase(status), + error_code: status.error_code ?? null, + component_id: status.component_id ?? null, + remediation: status.remediation ?? null, + local_path: status.local_path ?? null, + } +} + function getTauriBackendBridge(): MppBackendBridge | undefined { if (!isTauriRuntime()) return undefined if (tauriBackendBridge) return tauriBackendBridge @@ -169,6 +235,57 @@ export function getBackendBridge(): MppBackendBridge | undefined { return getTauriBackendBridge() } +export function getBootstrapBridge(): BootstrapBridge | undefined { + if (!isTauriRuntime()) return undefined + if (tauriBootstrapBridge) return tauriBootstrapBridge + + const getRawStatus = () => tauriInvoke("backend_get_status") + const getStatus = async () => normalizeBootstrapStatus(await getRawStatus()) + const getLogs = () => tauriInvoke("backend_get_logs") + + tauriBootstrapBridge = { + getStatus, + retry: async () => normalizeBootstrapStatus(await tauriInvoke("backend_retry")), + openLogs: () => tauriInvoke("open_logs"), + async getDiagnostics() { + const [status, logs] = await Promise.all([getStatus(), getLogs()]) + return { status, logs } + }, + onStatus(callback) { + let disposed = false + let lastStatus = "" + let timer: number | null = null + + const tick = async () => { + try { + const status = await getStatus() + const serialized = JSON.stringify(status) + if (!disposed && serialized !== lastStatus) { + lastStatus = serialized + callback(status) + } + } catch { + // The page keeps its last stable bootstrap state while one IPC poll is unavailable. + } finally { + if (!disposed) { + timer = window.setTimeout(() => void tick(), 750) + } + } + } + + void tick() + return () => { + disposed = true + if (timer !== null) { + window.clearTimeout(timer) + } + } + }, + } + + return tauriBootstrapBridge +} + export interface SelectDirectoryOptions { title?: string defaultPath?: string From 85c619afc49ee9f2bc8374cc9fbb7f05c8c5dd1e Mon Sep 17 00:00:00 2001 From: cmander02 <15946840899@163.com> Date: Tue, 28 Jul 2026 19:30:33 +0800 Subject: [PATCH 19/25] feat: expose bounded system diagnostics --- backend/app/api/routes/system.py | 30 ++ backend/app/cli/main.py | 42 ++- backend/app/main.py | 3 +- backend/app/services/system_diagnostics.py | 259 +++++++++++++++++ tests/test_system_diagnostics.py | 323 +++++++++++++++++++++ tests/test_system_diagnostics_api.py | 105 +++++++ 6 files changed, 760 insertions(+), 2 deletions(-) create mode 100644 backend/app/api/routes/system.py create mode 100644 backend/app/services/system_diagnostics.py create mode 100644 tests/test_system_diagnostics.py create mode 100644 tests/test_system_diagnostics_api.py diff --git a/backend/app/api/routes/system.py b/backend/app/api/routes/system.py new file mode 100644 index 0000000..3a75a8c --- /dev/null +++ b/backend/app/api/routes/system.py @@ -0,0 +1,30 @@ +"""Read-only system inspection endpoints.""" + +from __future__ import annotations + +import asyncio + +from fastapi import APIRouter, HTTPException +from fastapi.responses import JSONResponse + +from app.services.system_diagnostics import get_system_diagnostics + +router = APIRouter(prefix="/system", tags=["system"]) + +_NO_STORE_HEADERS = {"Cache-Control": "no-store"} + + +@router.get("/diagnostics") +async def system_diagnostics() -> JSONResponse: + try: + payload = await asyncio.to_thread(get_system_diagnostics) + except Exception: + raise HTTPException( + status_code=503, + detail="Runtime diagnostics are unavailable", + headers=_NO_STORE_HEADERS, + ) from None + return JSONResponse(payload, headers=_NO_STORE_HEADERS) + + +__all__ = ["router"] diff --git a/backend/app/cli/main.py b/backend/app/cli/main.py index 430562c..58bbce4 100644 --- a/backend/app/cli/main.py +++ b/backend/app/cli/main.py @@ -1382,8 +1382,48 @@ def config_preset( # --------------------------------------------------------------------------- @app.command() -def doctor(): +def doctor( + json_out: bool = typer.Option( + False, + "--json", + help="输出与 /api/system/diagnostics 相同的机器可读报告", + ), +): """检查运行环境(ffmpeg、CUDA、模型文件、API key 等)。""" + if json_out or _json_mode: + from app.services.system_diagnostics import ( + SYSTEM_DIAGNOSTICS_SCHEMA, + SYSTEM_DIAGNOSTICS_SCHEMA_VERSION, + get_system_diagnostics, + ) + + try: + payload = get_system_diagnostics() + except Exception: + payload = { + "error": "runtime-diagnostics-unavailable", + "schema": SYSTEM_DIAGNOSTICS_SCHEMA, + "schemaVersion": SYSTEM_DIAGNOSTICS_SCHEMA_VERSION, + } + typer.echo( + json.dumps( + payload, + ensure_ascii=False, + separators=(",", ":"), + sort_keys=True, + ) + ) + raise typer.Exit(code=1) from None + typer.echo( + json.dumps( + payload, + ensure_ascii=False, + separators=(",", ":"), + sort_keys=True, + ) + ) + return + import pathlib import shutil diff --git a/backend/app/main.py b/backend/app/main.py index be5d0e3..824670e 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -13,7 +13,7 @@ from fastapi.staticfiles import StaticFiles from starlette.background import BackgroundTask -from app.api.routes import filesystem, pipeline, sync, tasks, voiceprints +from app.api.routes import filesystem, pipeline, sync, system, tasks, voiceprints from app.api.routes import kb as kb_router from app.api.routes import settings as settings_router from app.core.config import get_settings @@ -301,6 +301,7 @@ async def auth_middleware(request: Request, call_next): app.include_router(voiceprints.router, prefix="/api") app.include_router(kb_router.router, prefix="/api") app.include_router(sync.router, prefix="/api") +app.include_router(system.router, prefix="/api") @app.post(_DESKTOP_SHUTDOWN_PATH) diff --git a/backend/app/services/system_diagnostics.py b/backend/app/services/system_diagnostics.py new file mode 100644 index 0000000..94a76b3 --- /dev/null +++ b/backend/app/services/system_diagnostics.py @@ -0,0 +1,259 @@ +"""Stable public diagnostics document shared by the CLI and HTTP API.""" + +from __future__ import annotations + +import hashlib +import json +import re +from typing import Any + +from app.services.runtime_diagnostics import ( + DIAGNOSTIC_SCHEMA_VERSION, + DiagnosticReport, + DiagnosticResult, + DiagnosticStatus, + list_diagnostic_capabilities, + run_runtime_diagnostics, +) +from app.version import APP_VERSION + +SYSTEM_DIAGNOSTICS_SCHEMA = "mpp.system-diagnostics" +SYSTEM_DIAGNOSTICS_SCHEMA_VERSION = 1 +SYSTEM_DIAGNOSTICS_PROBES = ( + "configuration", + "disk", + "fastapi", + "ffmpeg", + "ffprobe", + "openai", + "pillow", + "python", + "uv", + "writable-paths", +) +SYSTEM_DIAGNOSTICS_PER_PROBE_TIMEOUT_SECONDS = 5.0 +SYSTEM_DIAGNOSTICS_TOTAL_TIMEOUT_SECONDS = 15.0 +SYSTEM_DIAGNOSTICS_MAX_COMPONENTS = len(SYSTEM_DIAGNOSTICS_PROBES) +SYSTEM_DIAGNOSTICS_MAX_DETAIL_FIELDS = 16 +SYSTEM_DIAGNOSTICS_MAX_DETAIL_ITEMS = 16 +SYSTEM_DIAGNOSTICS_MAX_STRING_BYTES = 256 + +_MAX_SOURCE_STRING_BYTES = 8192 +_MAX_DETAIL_KEY_LENGTH = 64 +_REDACTED = "[REDACTED]" +_OUTPUT_REJECTED = "[OUTPUT_REJECTED]" +_DETAIL_KEY = re.compile(r"^[A-Za-z][A-Za-z0-9_-]{0,63}$") +_SENSITIVE_KEY = re.compile( + r"(?:api[_-]?key|authorization|bili[_-]?jct|cookie|csrf|" + r"password|refresh[_-]?token|secret|sessdata|token)", + re.IGNORECASE, +) +_SENSITIVE_VALUE = re.compile( + r"(?:" + r"authorization\s*:\s*bearer\s+\S+" + r"|(?:api[_-]?key|bili[_-]?jct|cookie|csrf|password|" + r"refresh[_-]?token|secret|sessdata|token)" + r"\s*[\"']?\s*[:=]\s*[\"']?[^\s\"',;}{]{4,}" + r"|(?:sk-|gh[oprsu]_|github_pat_|hf_)[A-Za-z0-9_-]{8,}" + r"|https?://[^/\s:@]+:[^/\s@]+@" + r")", + re.IGNORECASE, +) + + +def _bounded_text(value: str) -> str: + encoded = value.encode("utf-8", errors="replace") + if len(encoded) > _MAX_SOURCE_STRING_BYTES: + return _OUTPUT_REJECTED + normalized = "".join( + character if character >= " " and character != "\x7f" else " " + for character in encoded.decode("utf-8", errors="replace") + ) + if _SENSITIVE_VALUE.search(normalized): + return _REDACTED + normalized_bytes = normalized.encode("utf-8") + if len(normalized_bytes) <= SYSTEM_DIAGNOSTICS_MAX_STRING_BYTES: + return normalized + suffix = b"..." + prefix = normalized_bytes[ + : SYSTEM_DIAGNOSTICS_MAX_STRING_BYTES - len(suffix) + ].decode("utf-8", errors="ignore") + return prefix + suffix.decode("ascii") + + +def _safe_detail_value(value: Any) -> str | int | bool | list[str]: + if isinstance(value, bool): + return value + if isinstance(value, int): + return value + if isinstance(value, str): + return _bounded_text(value) + if isinstance(value, tuple): + return [ + _bounded_text(item) if isinstance(item, str) else _OUTPUT_REJECTED + for item in value[:SYSTEM_DIAGNOSTICS_MAX_DETAIL_ITEMS] + ] + return _OUTPUT_REJECTED + + +def _safe_details(result: DiagnosticResult) -> dict[str, str | int | bool | list[str]]: + details: dict[str, str | int | bool | list[str]] = {} + for raw_key, raw_value in result.details[:SYSTEM_DIAGNOSTICS_MAX_DETAIL_FIELDS]: + if not isinstance(raw_key, str) or len(raw_key) > _MAX_DETAIL_KEY_LENGTH: + continue + if _DETAIL_KEY.fullmatch(raw_key) is None: + continue + details[raw_key] = ( + _REDACTED + if _SENSITIVE_KEY.search(raw_key) + else _safe_detail_value(raw_value) + ) + return details + + +def _component_document( + result: DiagnosticResult, + *, + display_name: str, + category: str, +) -> dict[str, Any]: + status = ( + result.status.value + if isinstance(result.status, DiagnosticStatus) + else DiagnosticStatus.ERROR.value + ) + summary = ( + _bounded_text(result.summary) + if isinstance(result.summary, str) + else _OUTPUT_REJECTED + ) + return { + "category": _bounded_text(category), + "details": _safe_details(result), + "displayName": _bounded_text(display_name), + "id": result.probe_id, + "status": status, + "summary": summary, + } + + +def _component_summary(components: list[dict[str, Any]]) -> tuple[str, bool, bool]: + statuses = tuple(component["status"] for component in components) + if not statuses: + return "not-run", False, False + attention_statuses = { + DiagnosticStatus.DEGRADED.value, + DiagnosticStatus.ERROR.value, + DiagnosticStatus.TIMEOUT.value, + DiagnosticStatus.UNAVAILABLE.value, + DiagnosticStatus.UNTRUSTED.value, + } + verified = all(status == DiagnosticStatus.AVAILABLE.value for status in statuses) + healthy = all( + status + in { + DiagnosticStatus.AVAILABLE.value, + DiagnosticStatus.UNKNOWN.value, + } + for status in statuses + ) + if any(status in attention_statuses for status in statuses): + return "attention", healthy, verified + if any(status == DiagnosticStatus.UNKNOWN.value for status in statuses): + return "unknown", healthy, verified + return "available", healthy, verified + + +def build_system_diagnostics_document( + report: DiagnosticReport, + *, + app_version: str = APP_VERSION, +) -> dict[str, Any]: + """Build the bounded, deterministic diagnostics contract exposed publicly.""" + + if not isinstance(report, DiagnosticReport): + raise TypeError("report must be a DiagnosticReport") + + capabilities = { + capability.id: capability for capability in list_diagnostic_capabilities() + } + allowed_probes = frozenset(SYSTEM_DIAGNOSTICS_PROBES) + requested_probes = tuple( + probe_id + for probe_id in report.requested_probes + if isinstance(probe_id, str) and probe_id in allowed_probes + )[:SYSTEM_DIAGNOSTICS_MAX_COMPONENTS] + + components: list[dict[str, Any]] = [] + seen: set[str] = set() + for result in report.results: + if len(components) >= SYSTEM_DIAGNOSTICS_MAX_COMPONENTS: + break + if not isinstance(result, DiagnosticResult): + continue + if result.probe_id not in allowed_probes or result.probe_id in seen: + continue + capability = capabilities[result.probe_id] + components.append( + _component_document( + result, + display_name=capability.display_name, + category=capability.category, + ) + ) + seen.add(result.probe_id) + + status, healthy, verified = _component_summary(components) + payload: dict[str, Any] = { + "appVersion": _bounded_text(app_version), + "components": components, + "diagnosticSchemaVersion": DIAGNOSTIC_SCHEMA_VERSION, + "healthy": healthy, + "limits": { + "componentLimit": SYSTEM_DIAGNOSTICS_MAX_COMPONENTS, + "detailFieldLimit": SYSTEM_DIAGNOSTICS_MAX_DETAIL_FIELDS, + "detailItemLimit": SYSTEM_DIAGNOSTICS_MAX_DETAIL_ITEMS, + "perProbeTimeoutSeconds": SYSTEM_DIAGNOSTICS_PER_PROBE_TIMEOUT_SECONDS, + "stringByteLimit": SYSTEM_DIAGNOSTICS_MAX_STRING_BYTES, + "totalTimeoutSeconds": SYSTEM_DIAGNOSTICS_TOTAL_TIMEOUT_SECONDS, + }, + "requestedProbes": list(requested_probes), + "schema": SYSTEM_DIAGNOSTICS_SCHEMA, + "schemaVersion": SYSTEM_DIAGNOSTICS_SCHEMA_VERSION, + "status": status, + "verified": verified, + } + canonical = json.dumps( + payload, + ensure_ascii=False, + separators=(",", ":"), + sort_keys=True, + ).encode("utf-8") + payload["reportDigest"] = f"sha256:{hashlib.sha256(canonical).hexdigest()}" + return payload + + +def get_system_diagnostics() -> dict[str, Any]: + """Run the fixed public probe set within a bounded total deadline.""" + + report = run_runtime_diagnostics( + SYSTEM_DIAGNOSTICS_PROBES, + timeout_seconds=SYSTEM_DIAGNOSTICS_PER_PROBE_TIMEOUT_SECONDS, + total_timeout_seconds=SYSTEM_DIAGNOSTICS_TOTAL_TIMEOUT_SECONDS, + ) + return build_system_diagnostics_document(report) + + +__all__ = [ + "SYSTEM_DIAGNOSTICS_MAX_COMPONENTS", + "SYSTEM_DIAGNOSTICS_MAX_DETAIL_FIELDS", + "SYSTEM_DIAGNOSTICS_MAX_DETAIL_ITEMS", + "SYSTEM_DIAGNOSTICS_MAX_STRING_BYTES", + "SYSTEM_DIAGNOSTICS_PER_PROBE_TIMEOUT_SECONDS", + "SYSTEM_DIAGNOSTICS_PROBES", + "SYSTEM_DIAGNOSTICS_SCHEMA", + "SYSTEM_DIAGNOSTICS_SCHEMA_VERSION", + "SYSTEM_DIAGNOSTICS_TOTAL_TIMEOUT_SECONDS", + "build_system_diagnostics_document", + "get_system_diagnostics", +] diff --git a/tests/test_system_diagnostics.py b/tests/test_system_diagnostics.py new file mode 100644 index 0000000..f33624d --- /dev/null +++ b/tests/test_system_diagnostics.py @@ -0,0 +1,323 @@ +from __future__ import annotations + +import hashlib +import json +import shutil +import sys +from pathlib import Path +from types import SimpleNamespace + +import pytest +from typer.testing import CliRunner + +ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(ROOT / "backend")) + +from app.cli import main as cli_main # noqa: E402 +from app.services import system_diagnostics # noqa: E402 +from app.services.runtime_diagnostics import ( # noqa: E402 + DIAGNOSTIC_SCHEMA_VERSION, + DiagnosticReport, + DiagnosticResult, + DiagnosticStatus, +) + + +def _report(*results: DiagnosticResult) -> DiagnosticReport: + return DiagnosticReport( + schema=DIAGNOSTIC_SCHEMA_VERSION, + requested_probes=tuple(result.probe_id for result in results), + results=results, + ) + + +def _payload() -> dict: + return { + "appVersion": "0.4.1", + "components": [], + "diagnosticSchemaVersion": 1, + "healthy": True, + "limits": {}, + "reportDigest": "sha256:" + "a" * 64, + "requestedProbes": [], + "schema": "mpp.system-diagnostics", + "schemaVersion": 1, + "status": "available", + "verified": True, + } + + +def test_system_diagnostics_document_has_stable_schema_and_digest() -> None: + report = _report( + DiagnosticResult( + "python", + DiagnosticStatus.AVAILABLE, + "Python runtime is available.", + (("version", "3.12.8"),), + ), + DiagnosticResult( + "pillow", + DiagnosticStatus.AVAILABLE, + "Pillow image codecs are available.", + (("formats", ("jpeg", "png", "webp")),), + ), + ) + + first = system_diagnostics.build_system_diagnostics_document( + report, + app_version="0.4.1", + ) + second = system_diagnostics.build_system_diagnostics_document( + report, + app_version="0.4.1", + ) + + assert first == second + assert first["schema"] == system_diagnostics.SYSTEM_DIAGNOSTICS_SCHEMA + assert first["schemaVersion"] == 1 + assert first["diagnosticSchemaVersion"] == DIAGNOSTIC_SCHEMA_VERSION + assert first["status"] == "available" + assert first["healthy"] is True + assert first["verified"] is True + assert [component["id"] for component in first["components"]] == [ + "python", + "pillow", + ] + + digest = first["reportDigest"] + unsigned = dict(first) + del unsigned["reportDigest"] + canonical = json.dumps( + unsigned, + ensure_ascii=False, + separators=(",", ":"), + sort_keys=True, + ).encode("utf-8") + assert digest == f"sha256:{hashlib.sha256(canonical).hexdigest()}" + + +def test_public_probe_set_matches_remote_api_core_profile() -> None: + assert system_diagnostics.SYSTEM_DIAGNOSTICS_PROBES == ( + "configuration", + "disk", + "fastapi", + "ffmpeg", + "ffprobe", + "openai", + "pillow", + "python", + "uv", + "writable-paths", + ) + + +def test_document_summary_is_derived_only_from_published_components() -> None: + report = DiagnosticReport( + schema=DIAGNOSTIC_SCHEMA_VERSION, + requested_probes=("python", "cuda"), + results=( + DiagnosticResult( + "python", + DiagnosticStatus.AVAILABLE, + "Python runtime is available.", + ), + DiagnosticResult( + "cuda", + DiagnosticStatus.ERROR, + "CUDA failed outside the public core profile.", + ), + ), + ) + + payload = system_diagnostics.build_system_diagnostics_document(report) + + assert [component["id"] for component in payload["components"]] == ["python"] + assert payload["status"] == "available" + assert payload["healthy"] is True + assert payload["verified"] is True + + +@pytest.mark.parametrize( + "diagnostic_status", + [DiagnosticStatus.UNAVAILABLE, DiagnosticStatus.UNTRUSTED], +) +def test_public_binary_failure_is_preserved( + diagnostic_status: DiagnosticStatus, +) -> None: + report = _report( + DiagnosticResult( + "ffmpeg", + diagnostic_status, + "Signed runtime manifest trust is unavailable.", + ) + ) + + payload = system_diagnostics.build_system_diagnostics_document(report) + + assert payload["components"][0]["id"] == "ffmpeg" + assert payload["components"][0]["status"] == diagnostic_status.value + assert payload["status"] == "attention" + assert payload["healthy"] is False + assert payload["verified"] is False + + +def test_system_diagnostics_document_redacts_secrets_and_bounds_output() -> None: + details = ( + ("apiKey", "sk-sensitive-credential-value"), + ("authorization", "Bearer another-sensitive-value"), + ("note", "password=hunter2"), + ("longText", "界" * 300), + ("items", tuple(f"item-{index}" for index in range(40))), + *((f"field{index}", f"value-{index}") for index in range(30)), + ) + report = _report( + DiagnosticResult( + "python", + DiagnosticStatus.AVAILABLE, + "Authorization: Bearer top-secret-token", + details, + ) + ) + + payload = system_diagnostics.build_system_diagnostics_document(report) + encoded = json.dumps(payload, ensure_ascii=False) + + assert "sensitive-credential-value" not in encoded + assert "another-sensitive-value" not in encoded + assert "hunter2" not in encoded + assert "top-secret-token" not in encoded + component = payload["components"][0] + assert component["summary"] == "[REDACTED]" + assert component["details"]["apiKey"] == "[REDACTED]" + assert component["details"]["authorization"] == "[REDACTED]" + assert component["details"]["note"] == "[REDACTED]" + assert len(component["details"]) <= system_diagnostics.SYSTEM_DIAGNOSTICS_MAX_DETAIL_FIELDS + assert ( + len(component["details"]["items"]) + <= system_diagnostics.SYSTEM_DIAGNOSTICS_MAX_DETAIL_ITEMS + ) + + def strings(value): + if isinstance(value, str): + yield value + elif isinstance(value, dict): + for key, item in value.items(): + yield key + yield from strings(item) + elif isinstance(value, list): + for item in value: + yield from strings(item) + + assert all( + len(value.encode("utf-8")) + <= system_diagnostics.SYSTEM_DIAGNOSTICS_MAX_STRING_BYTES + for value in strings(payload) + ) + + +def test_get_system_diagnostics_uses_fixed_probes_and_deadlines( + monkeypatch: pytest.MonkeyPatch, +) -> None: + captured: dict = {} + report = _report( + DiagnosticResult( + "python", + DiagnosticStatus.AVAILABLE, + "Python runtime is available.", + ) + ) + + def run(probes, **kwargs): + captured["probes"] = probes + captured.update(kwargs) + return report + + monkeypatch.setattr(system_diagnostics, "run_runtime_diagnostics", run) + + payload = system_diagnostics.get_system_diagnostics() + + assert captured == { + "probes": system_diagnostics.SYSTEM_DIAGNOSTICS_PROBES, + "timeout_seconds": ( + system_diagnostics.SYSTEM_DIAGNOSTICS_PER_PROBE_TIMEOUT_SECONDS + ), + "total_timeout_seconds": ( + system_diagnostics.SYSTEM_DIAGNOSTICS_TOTAL_TIMEOUT_SECONDS + ), + } + assert payload["components"][0]["id"] == "python" + + +@pytest.mark.parametrize("arguments", [["doctor", "--json"], ["--json", "doctor"]]) +def test_doctor_json_uses_shared_diagnostics_service( + arguments: list[str], + monkeypatch: pytest.MonkeyPatch, +) -> None: + payload = _payload() + monkeypatch.setattr( + system_diagnostics, + "get_system_diagnostics", + lambda: payload, + ) + + result = CliRunner().invoke(cli_main.app, arguments) + + assert result.exit_code == 0 + assert json.loads(result.stdout) == payload + assert "Daemon" not in result.stdout + + +def test_doctor_json_failure_is_machine_readable_and_does_not_echo_exception( + monkeypatch: pytest.MonkeyPatch, +) -> None: + def fail(): + raise RuntimeError("api_key=do-not-print-this") + + monkeypatch.setattr(system_diagnostics, "get_system_diagnostics", fail) + + result = CliRunner().invoke(cli_main.app, ["doctor", "--json"]) + + assert result.exit_code == 1 + assert json.loads(result.stdout) == { + "error": "runtime-diagnostics-unavailable", + "schema": system_diagnostics.SYSTEM_DIAGNOSTICS_SCHEMA, + "schemaVersion": system_diagnostics.SYSTEM_DIAGNOSTICS_SCHEMA_VERSION, + } + assert "do-not-print-this" not in result.output + + +def test_doctor_interactive_output_remains_compatible( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr( + cli_main, + "_get_client", + lambda: SimpleNamespace(ping=lambda: True), + ) + monkeypatch.setattr( + cli_main, + "_read_settings", + lambda: { + "data_root": str(tmp_path), + "llm_provider": "deepseek", + "qwen3_asr_model_path": "", + }, + ) + monkeypatch.setattr(shutil, "which", lambda _name: "C:\\tools\\ffmpeg.exe") + monkeypatch.setitem( + sys.modules, + "torch", + SimpleNamespace( + cuda=SimpleNamespace( + is_available=lambda: True, + get_device_name=lambda _index: "Test GPU", + ) + ), + ) + + result = CliRunner().invoke(cli_main.app, ["--plain", "doctor"]) + + assert result.exit_code == 0 + for label in ("Daemon", "ffmpeg", "CUDA", "data_root", "LLM", "ASR model"): + assert label in result.stdout diff --git a/tests/test_system_diagnostics_api.py b/tests/test_system_diagnostics_api.py new file mode 100644 index 0000000..6db708c --- /dev/null +++ b/tests/test_system_diagnostics_api.py @@ -0,0 +1,105 @@ +from __future__ import annotations + +import json +import sys +from pathlib import Path +from types import SimpleNamespace + +from fastapi import FastAPI +from fastapi.testclient import TestClient + +ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(ROOT / "backend")) + +from app.api.routes import system # noqa: E402 + + +def _payload() -> dict: + return { + "appVersion": "0.4.1", + "components": [], + "diagnosticSchemaVersion": 1, + "healthy": True, + "limits": {}, + "reportDigest": "sha256:" + "a" * 64, + "requestedProbes": [], + "schema": "mpp.system-diagnostics", + "schemaVersion": 1, + "status": "available", + "verified": True, + } + + +def test_system_diagnostics_route_is_read_only_and_disables_caching( + monkeypatch, +) -> None: + payload = _payload() + monkeypatch.setattr(system, "get_system_diagnostics", lambda: payload) + app = FastAPI() + app.include_router(system.router, prefix="/api") + + with TestClient(app) as client: + response = client.get("/api/system/diagnostics") + mutation = client.post("/api/system/diagnostics") + + assert response.status_code == 200 + assert response.json() == payload + assert response.headers["cache-control"] == "no-store" + assert mutation.status_code == 405 + + +def test_system_diagnostics_route_returns_fixed_error_without_exception_data( + monkeypatch, +) -> None: + def fail(): + raise RuntimeError("Authorization: Bearer do-not-return-this") + + monkeypatch.setattr(system, "get_system_diagnostics", fail) + app = FastAPI() + app.include_router(system.router, prefix="/api") + + with TestClient(app) as client: + response = client.get("/api/system/diagnostics") + + assert response.status_code == 503 + assert response.json() == {"detail": "Runtime diagnostics are unavailable"} + assert response.headers["cache-control"] == "no-store" + assert "do-not-return-this" not in json.dumps(response.json()) + + +def test_main_app_mounts_diagnostics_under_existing_api_authentication( + monkeypatch, +) -> None: + from app import main as app_main + + payload = _payload() + monkeypatch.delenv("MPP_REQUIRE_API_TOKEN", raising=False) + monkeypatch.setattr(app_main, "_DESKTOP_SESSION_SECRET", "") + monkeypatch.setattr( + app_main, + "get_runtime_settings", + lambda: SimpleNamespace(api_token="diagnostics-test-token"), + ) + monkeypatch.setattr(system, "get_system_diagnostics", lambda: payload) + + client = TestClient(app_main.app) + try: + unauthorized = client.get("/api/system/diagnostics") + authorized = client.get( + "/api/system/diagnostics", + headers={"Authorization": "Bearer diagnostics-test-token"}, + ) + mutation = client.post( + "/api/system/diagnostics", + headers={ + "Authorization": "Bearer diagnostics-test-token", + "X-Requested-With": "XMLHttpRequest", + }, + ) + finally: + client.close() + + assert unauthorized.status_code == 401 + assert authorized.status_code == 200 + assert authorized.json() == payload + assert mutation.status_code == 405 From 516de2e758ecc4c0ccab9a9af18d291979717ced Mon Sep 17 00:00:00 2001 From: cmander02 <15946840899@163.com> Date: Tue, 28 Jul 2026 20:12:07 +0800 Subject: [PATCH 20/25] feat: add native desktop preflight checks --- backend/app/services/settings_preflight.py | 141 + tests/test_settings_preflight.py | 165 ++ web/src-tauri/capabilities/default.json | 1 + web/src-tauri/gen/schemas/acl-manifests.json | 2 +- web/src-tauri/gen/schemas/capabilities.json | 2 +- web/src-tauri/gen/schemas/desktop-schema.json | 6 + web/src-tauri/gen/schemas/windows-schema.json | 6 + web/src-tauri/permissions/backend.toml | 5 + web/src-tauri/src/main.rs | 2375 ++++++++++++++++- .../components/pages/bootstrap-page.test.tsx | 123 +- web/src/components/pages/bootstrap-page.tsx | 177 +- .../bootstrap-preflight-scanning-v1.json | 116 + web/src/lib/tauri.test.ts | 104 +- web/src/lib/tauri.ts | 178 ++ 14 files changed, 3292 insertions(+), 109 deletions(-) create mode 100644 backend/app/services/settings_preflight.py create mode 100644 tests/test_settings_preflight.py create mode 100644 web/src/lib/fixtures/bootstrap-preflight-scanning-v1.json diff --git a/backend/app/services/settings_preflight.py b/backend/app/services/settings_preflight.py new file mode 100644 index 0000000..db9f657 --- /dev/null +++ b/backend/app/services/settings_preflight.py @@ -0,0 +1,141 @@ +"""Read-only, bounded runtime-settings validation for the desktop bootstrap. + +The command contract is intentionally tiny and credential-safe. It emits +exactly one fixed token and never includes configuration values, paths, or +exception messages. +""" + +from __future__ import annotations + +import json +import os +import stat +import sys +from enum import Enum +from pathlib import Path +from typing import Sequence + +if __package__ in {None, ""}: + sys.path.insert(0, str(Path(__file__).resolve().parents[2])) + +from app.core.settings import ( # noqa: E402 + RuntimeSettings, + _normalize_custom_profile_state, + _normalize_settings_document_state, +) + +SETTINGS_PREFLIGHT_SCHEMA_VERSION = 1 +MAX_SETTINGS_BYTES = 1024 * 1024 +SETTINGS_PREFLIGHT_OK_TOKEN = "MPP_SETTINGS_PREFLIGHT_V1_OK" +SETTINGS_PREFLIGHT_INVALID_TOKEN = "MPP_SETTINGS_PREFLIGHT_V1_INVALID" +SETTINGS_PREFLIGHT_ERROR_TOKEN = "MPP_SETTINGS_PREFLIGHT_V1_ERROR" + + +class SettingsPreflightStatus(Enum): + VALID = ("valid", 0, SETTINGS_PREFLIGHT_OK_TOKEN) + MISSING = ("missing", 0, SETTINGS_PREFLIGHT_OK_TOKEN) + INVALID = ("invalid", 2, SETTINGS_PREFLIGHT_INVALID_TOKEN) + TOO_LARGE = ("too-large", 2, SETTINGS_PREFLIGHT_INVALID_TOKEN) + UNAVAILABLE = ("unavailable", 2, SETTINGS_PREFLIGHT_INVALID_TOKEN) + INTERNAL_ERROR = ("internal-error", 70, SETTINGS_PREFLIGHT_ERROR_TOKEN) + USAGE_ERROR = ("usage-error", 64, SETTINGS_PREFLIGHT_ERROR_TOKEN) + + def __init__(self, label: str, exit_code: int, token: str) -> None: + self.label = label + self.exit_code = exit_code + self.token = token + + +def _is_link_or_reparse(path: Path, metadata: os.stat_result) -> bool: + if path.is_symlink(): + return True + file_attributes = getattr(metadata, "st_file_attributes", 0) + return bool(file_attributes & 0x0400) + + +def _read_bounded_settings(path: Path) -> tuple[SettingsPreflightStatus | None, bytes]: + try: + metadata = path.lstat() + except FileNotFoundError: + return SettingsPreflightStatus.MISSING, b"" + except OSError: + return SettingsPreflightStatus.UNAVAILABLE, b"" + + if ( + _is_link_or_reparse(path, metadata) + or not stat.S_ISREG(metadata.st_mode) + ): + return SettingsPreflightStatus.UNAVAILABLE, b"" + if metadata.st_size > MAX_SETTINGS_BYTES: + return SettingsPreflightStatus.TOO_LARGE, b"" + + try: + with path.open("rb") as file: + content = file.read(MAX_SETTINGS_BYTES + 1) + except OSError: + return SettingsPreflightStatus.UNAVAILABLE, b"" + if len(content) > MAX_SETTINGS_BYTES: + return SettingsPreflightStatus.TOO_LARGE, b"" + return None, content + + +def preflight_runtime_settings(path: Path) -> SettingsPreflightStatus: + """Validate one settings file without changing process or filesystem state.""" + + if not isinstance(path, Path) or not path.is_absolute(): + return SettingsPreflightStatus.UNAVAILABLE + + read_status, content = _read_bounded_settings(path) + if read_status is SettingsPreflightStatus.MISSING: + try: + RuntimeSettings() + except Exception: + return SettingsPreflightStatus.INTERNAL_ERROR + return SettingsPreflightStatus.MISSING + if read_status is not None: + return read_status + + try: + document = json.loads(content.decode("utf-8")) + except (UnicodeDecodeError, json.JSONDecodeError): + return SettingsPreflightStatus.INVALID + if not isinstance(document, dict): + return SettingsPreflightStatus.INVALID + + try: + _normalize_custom_profile_state(document, prefer_profiles=True) + _normalize_settings_document_state(document) + RuntimeSettings(**document) + except Exception: + return SettingsPreflightStatus.INVALID + return SettingsPreflightStatus.VALID + + +def main(argv: Sequence[str] | None = None) -> int: + arguments = list(sys.argv[1:] if argv is None else argv) + if len(arguments) != 1: + status = SettingsPreflightStatus.USAGE_ERROR + else: + try: + path = Path(arguments[0]) + status = preflight_runtime_settings(path) + except BaseException: + status = SettingsPreflightStatus.INTERNAL_ERROR + sys.stdout.write(f"{status.token}\n") + return status.exit_code + + +if __name__ == "__main__": + raise SystemExit(main()) + + +__all__ = [ + "MAX_SETTINGS_BYTES", + "SETTINGS_PREFLIGHT_ERROR_TOKEN", + "SETTINGS_PREFLIGHT_INVALID_TOKEN", + "SETTINGS_PREFLIGHT_OK_TOKEN", + "SETTINGS_PREFLIGHT_SCHEMA_VERSION", + "SettingsPreflightStatus", + "main", + "preflight_runtime_settings", +] diff --git a/tests/test_settings_preflight.py b/tests/test_settings_preflight.py new file mode 100644 index 0000000..fd56356 --- /dev/null +++ b/tests/test_settings_preflight.py @@ -0,0 +1,165 @@ +from __future__ import annotations + +import json +import subprocess +import sys +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(ROOT / "backend")) + +from app.services.settings_preflight import ( # noqa: E402 + MAX_SETTINGS_BYTES, + SETTINGS_PREFLIGHT_ERROR_TOKEN, + SETTINGS_PREFLIGHT_INVALID_TOKEN, + SETTINGS_PREFLIGHT_OK_TOKEN, + SettingsPreflightStatus, + main, + preflight_runtime_settings, +) + + +def test_valid_settings_reuse_runtime_normalization_without_mutating_file( + tmp_path: Path, +) -> None: + path = tmp_path / "config.json" + path.write_text( + json.dumps( + { + "custom_active_profile_id": "legacy", + "custom_name": "Legacy provider", + "custom_api_base": "https://example.invalid/v1", + "custom_model": "model", + "max_download_concurrency": 2, + } + ), + encoding="utf-8", + ) + before = path.read_bytes() + + status = preflight_runtime_settings(path) + + assert status is SettingsPreflightStatus.VALID + assert path.read_bytes() == before + + +def test_semantically_invalid_settings_are_rejected_without_exposing_secrets( + tmp_path: Path, +) -> None: + path = tmp_path / "config.json" + secret = "sk-secret-value-must-not-appear" + path.write_text( + json.dumps( + { + "openai_api_key": secret, + "max_download_concurrency": "definitely-invalid", + } + ), + encoding="utf-8", + ) + + completed = subprocess.run( + [ + sys.executable, + "-I", + str(ROOT / "backend" / "app" / "services" / "settings_preflight.py"), + str(path), + ], + cwd=tmp_path, + stdin=subprocess.DEVNULL, + capture_output=True, + text=True, + encoding="utf-8", + check=False, + ) + + assert completed.returncode == 2 + assert completed.stdout == f"{SETTINGS_PREFLIGHT_INVALID_TOKEN}\n" + assert completed.stderr == "" + assert secret not in completed.stdout + completed.stderr + + +@pytest.mark.parametrize("document", [[], "settings", 1, None]) +def test_non_object_json_is_invalid(tmp_path: Path, document: object) -> None: + path = tmp_path / "config.json" + path.write_text(json.dumps(document), encoding="utf-8") + + assert preflight_runtime_settings(path) is SettingsPreflightStatus.INVALID + + +def test_oversized_settings_are_rejected_before_parsing(tmp_path: Path) -> None: + path = tmp_path / "config.json" + path.write_bytes(b"{" + b" " * MAX_SETTINGS_BYTES + b"}") + + assert preflight_runtime_settings(path) is SettingsPreflightStatus.TOO_LARGE + + +def test_missing_settings_use_runtime_defaults(tmp_path: Path) -> None: + path = tmp_path / "missing.json" + + status = preflight_runtime_settings(path) + + assert status is SettingsPreflightStatus.MISSING + assert status.exit_code == 0 + assert status.token == SETTINGS_PREFLIGHT_OK_TOKEN + assert not path.exists() + + +def test_invalid_utf8_and_invalid_json_use_the_fixed_invalid_contract( + tmp_path: Path, +) -> None: + invalid_utf8 = tmp_path / "invalid-utf8.json" + invalid_utf8.write_bytes(b"{\xff}") + invalid_json = tmp_path / "invalid-json.json" + invalid_json.write_text("{ invalid", encoding="utf-8") + + assert ( + preflight_runtime_settings(invalid_utf8) + is SettingsPreflightStatus.INVALID + ) + assert ( + preflight_runtime_settings(invalid_json) + is SettingsPreflightStatus.INVALID + ) + assert SettingsPreflightStatus.INVALID.exit_code == 2 + assert SettingsPreflightStatus.INVALID.token == SETTINGS_PREFLIGHT_INVALID_TOKEN + + +def test_cli_usage_and_internal_failures_emit_only_the_fixed_error_token( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + assert main([]) == SettingsPreflightStatus.USAGE_ERROR.exit_code + usage = capsys.readouterr() + assert usage.out == f"{SETTINGS_PREFLIGHT_ERROR_TOKEN}\n" + assert usage.err == "" + + def fail(_path: Path) -> SettingsPreflightStatus: + raise RuntimeError("api_key=must-not-be-emitted") + + monkeypatch.setattr( + "app.services.settings_preflight.preflight_runtime_settings", + fail, + ) + assert main([str(tmp_path / "config.json")]) == 70 + internal = capsys.readouterr() + assert internal.out == f"{SETTINGS_PREFLIGHT_ERROR_TOKEN}\n" + assert internal.err == "" + + +def test_desktop_runtime_accepts_the_same_fixed_tokens() -> None: + rust_source = ( + ROOT / "web" / "src-tauri" / "src" / "main.rs" + ).read_text(encoding="utf-8") + + assert ( + f'const SETTINGS_PREFLIGHT_OK_TOKEN: &str = ' + f'"{SETTINGS_PREFLIGHT_OK_TOKEN}";' + ) in rust_source + assert ( + f'const SETTINGS_PREFLIGHT_INVALID_TOKEN: &str = ' + f'"{SETTINGS_PREFLIGHT_INVALID_TOKEN}";' + ) in rust_source diff --git a/web/src-tauri/capabilities/default.json b/web/src-tauri/capabilities/default.json index c41e5c2..9f2e0a4 100644 --- a/web/src-tauri/capabilities/default.json +++ b/web/src-tauri/capabilities/default.json @@ -7,6 +7,7 @@ "permissions": [ "core:default", "allow-backend-get-status", + "allow-bootstrap-get-preflight", "allow-backend-get-logs", "allow-backend-start", "allow-backend-stop", diff --git a/web/src-tauri/gen/schemas/acl-manifests.json b/web/src-tauri/gen/schemas/acl-manifests.json index 2618c42..cca4734 100644 --- a/web/src-tauri/gen/schemas/acl-manifests.json +++ b/web/src-tauri/gen/schemas/acl-manifests.json @@ -1 +1 @@ -{"__app-acl__":{"default_permission":null,"permissions":{"allow-backend-get-logs":{"identifier":"allow-backend-get-logs","description":"Allows reading the desktop-managed backend process logs.","commands":{"allow":["backend_get_logs"],"deny":[]}},"allow-backend-get-status":{"identifier":"allow-backend-get-status","description":"Allows reading the desktop-managed backend process status.","commands":{"allow":["backend_get_status"],"deny":[]}},"allow-backend-restart":{"identifier":"allow-backend-restart","description":"Allows restarting the desktop-managed backend process.","commands":{"allow":["backend_restart"],"deny":[]}},"allow-backend-retry":{"identifier":"allow-backend-retry","description":"Allows retrying the fixed desktop bootstrap sequence.","commands":{"allow":["backend_retry"],"deny":[]}},"allow-backend-start":{"identifier":"allow-backend-start","description":"Allows starting the desktop-managed backend process.","commands":{"allow":["backend_start"],"deny":[]}},"allow-backend-stop":{"identifier":"allow-backend-stop","description":"Allows stopping the desktop-managed backend process.","commands":{"allow":["backend_stop"],"deny":[]}},"allow-open-external-url":{"identifier":"allow-open-external-url","description":"Allows opening http(s) source links in the system default browser.","commands":{"allow":["open_external_url"],"deny":[]}},"allow-open-logs":{"identifier":"allow-open-logs","description":"Allows opening the desktop-owned log directory.","commands":{"allow":["open_logs"],"deny":[]}}},"permission_sets":{},"global_scope_schema":null},"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener","allow-supports-multiple-windows"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-supports-multiple-windows":{"identifier":"allow-supports-multiple-windows","description":"Enables the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":["supports_multiple_windows"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"Denies the identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["identifier"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-data-store":{"identifier":"deny-remove-data-store","description":"Denies the remove_data_store command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_data_store"]}},"deny-remove-listener":{"identifier":"deny-remove-listener","description":"Denies the remove_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_listener"]}},"deny-set-app-theme":{"identifier":"deny-set-app-theme","description":"Denies the set_app_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_app_theme"]}},"deny-set-dock-visibility":{"identifier":"deny-set-dock-visibility","description":"Denies the set_dock_visibility command without any pre-configured scope.","commands":{"allow":[],"deny":["set_dock_visibility"]}},"deny-supports-multiple-windows":{"identifier":"deny-supports-multiple-windows","description":"Denies the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["supports_multiple_windows"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-icon-with-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-icon-with-as-template":{"identifier":"allow-set-icon-with-as-template","description":"Enables the set_icon_with_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_with_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-icon-with-as-template":{"identifier":"deny-set-icon-with-as-template","description":"Denies the set_icon_with_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_with_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-clear-all-browsing-data":{"identifier":"allow-clear-all-browsing-data","description":"Enables the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":["clear_all_browsing_data"],"deny":[]}},"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-auto-resize":{"identifier":"allow-set-webview-auto-resize","description":"Enables the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":["set_webview_auto_resize"],"deny":[]}},"allow-set-webview-background-color":{"identifier":"allow-set-webview-background-color","description":"Enables the set_webview_background_color command without any pre-configured scope.","commands":{"allow":["set_webview_background_color"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-hide":{"identifier":"allow-webview-hide","description":"Enables the webview_hide command without any pre-configured scope.","commands":{"allow":["webview_hide"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-show":{"identifier":"allow-webview-show","description":"Enables the webview_show command without any pre-configured scope.","commands":{"allow":["webview_show"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-clear-all-browsing-data":{"identifier":"deny-clear-all-browsing-data","description":"Denies the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":[],"deny":["clear_all_browsing_data"]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-auto-resize":{"identifier":"deny-set-webview-auto-resize","description":"Denies the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_auto_resize"]}},"deny-set-webview-background-color":{"identifier":"deny-set-webview-background-color","description":"Denies the set_webview_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_background_color"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-hide":{"identifier":"deny-webview-hide","description":"Denies the webview_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_hide"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-show":{"identifier":"deny-webview-show","description":"Denies the webview_show command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_show"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-is-enabled","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-is-always-on-top","allow-activity-name","allow-scene-identifier","allow-internal-toggle-maximize"]},"permissions":{"allow-activity-name":{"identifier":"allow-activity-name","description":"Enables the activity_name command without any pre-configured scope.","commands":{"allow":["activity_name"],"deny":[]}},"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-always-on-top":{"identifier":"allow-is-always-on-top","description":"Enables the is_always_on_top command without any pre-configured scope.","commands":{"allow":["is_always_on_top"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-scene-identifier":{"identifier":"allow-scene-identifier","description":"Enables the scene_identifier command without any pre-configured scope.","commands":{"allow":["scene_identifier"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-background-color":{"identifier":"allow-set-background-color","description":"Enables the set_background_color command without any pre-configured scope.","commands":{"allow":["set_background_color"],"deny":[]}},"allow-set-badge-count":{"identifier":"allow-set-badge-count","description":"Enables the set_badge_count command without any pre-configured scope.","commands":{"allow":["set_badge_count"],"deny":[]}},"allow-set-badge-label":{"identifier":"allow-set-badge-label","description":"Enables the set_badge_label command without any pre-configured scope.","commands":{"allow":["set_badge_label"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-focusable":{"identifier":"allow-set-focusable","description":"Enables the set_focusable command without any pre-configured scope.","commands":{"allow":["set_focusable"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-overlay-icon":{"identifier":"allow-set-overlay-icon","description":"Enables the set_overlay_icon command without any pre-configured scope.","commands":{"allow":["set_overlay_icon"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-simple-fullscreen":{"identifier":"allow-set-simple-fullscreen","description":"Enables the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":["set_simple_fullscreen"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-theme":{"identifier":"allow-set-theme","description":"Enables the set_theme command without any pre-configured scope.","commands":{"allow":["set_theme"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-activity-name":{"identifier":"deny-activity-name","description":"Denies the activity_name command without any pre-configured scope.","commands":{"allow":[],"deny":["activity_name"]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-always-on-top":{"identifier":"deny-is-always-on-top","description":"Denies the is_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["is_always_on_top"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-scene-identifier":{"identifier":"deny-scene-identifier","description":"Denies the scene_identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["scene_identifier"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-background-color":{"identifier":"deny-set-background-color","description":"Denies the set_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_background_color"]}},"deny-set-badge-count":{"identifier":"deny-set-badge-count","description":"Denies the set_badge_count command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_count"]}},"deny-set-badge-label":{"identifier":"deny-set-badge-label","description":"Denies the set_badge_label command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_label"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-focusable":{"identifier":"deny-set-focusable","description":"Denies the set_focusable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focusable"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-overlay-icon":{"identifier":"deny-set-overlay-icon","description":"Denies the set_overlay_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_overlay_icon"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-simple-fullscreen":{"identifier":"deny-set-simple-fullscreen","description":"Denies the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_simple_fullscreen"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-theme":{"identifier":"deny-set-theme","description":"Denies the set_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_theme"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null},"dialog":{"default_permission":{"identifier":"default","description":"This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n","permissions":["allow-message","allow-save","allow-open"]},"permissions":{"allow-ask":{"identifier":"allow-ask","description":"Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)","commands":{"allow":["message"],"deny":[]}},"allow-confirm":{"identifier":"allow-confirm","description":"Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)","commands":{"allow":["message"],"deny":[]}},"allow-message":{"identifier":"allow-message","description":"Enables the message command without any pre-configured scope.","commands":{"allow":["message"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-save":{"identifier":"allow-save","description":"Enables the save command without any pre-configured scope.","commands":{"allow":["save"],"deny":[]}},"deny-ask":{"identifier":"deny-ask","description":"Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)","commands":{"allow":[],"deny":["message"]}},"deny-confirm":{"identifier":"deny-confirm","description":"Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)","commands":{"allow":[],"deny":["message"]}},"deny-message":{"identifier":"deny-message","description":"Denies the message command without any pre-configured scope.","commands":{"allow":[],"deny":["message"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-save":{"identifier":"deny-save","description":"Denies the save command without any pre-configured scope.","commands":{"allow":[],"deny":["save"]}}},"permission_sets":{},"global_scope_schema":null}} \ No newline at end of file +{"__app-acl__":{"default_permission":null,"permissions":{"allow-backend-get-logs":{"identifier":"allow-backend-get-logs","description":"Allows reading the desktop-managed backend process logs.","commands":{"allow":["backend_get_logs"],"deny":[]}},"allow-backend-get-status":{"identifier":"allow-backend-get-status","description":"Allows reading the desktop-managed backend process status.","commands":{"allow":["backend_get_status"],"deny":[]}},"allow-backend-restart":{"identifier":"allow-backend-restart","description":"Allows restarting the desktop-managed backend process.","commands":{"allow":["backend_restart"],"deny":[]}},"allow-backend-retry":{"identifier":"allow-backend-retry","description":"Allows retrying the fixed desktop bootstrap sequence.","commands":{"allow":["backend_retry"],"deny":[]}},"allow-backend-start":{"identifier":"allow-backend-start","description":"Allows starting the desktop-managed backend process.","commands":{"allow":["backend_start"],"deny":[]}},"allow-backend-stop":{"identifier":"allow-backend-stop","description":"Allows stopping the desktop-managed backend process.","commands":{"allow":["backend_stop"],"deny":[]}},"allow-bootstrap-get-preflight":{"identifier":"allow-bootstrap-get-preflight","description":"Allows reading the cached desktop bootstrap preflight report.","commands":{"allow":["bootstrap_get_preflight"],"deny":[]}},"allow-open-external-url":{"identifier":"allow-open-external-url","description":"Allows opening http(s) source links in the system default browser.","commands":{"allow":["open_external_url"],"deny":[]}},"allow-open-logs":{"identifier":"allow-open-logs","description":"Allows opening the desktop-owned log directory.","commands":{"allow":["open_logs"],"deny":[]}}},"permission_sets":{},"global_scope_schema":null},"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener","allow-supports-multiple-windows"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-supports-multiple-windows":{"identifier":"allow-supports-multiple-windows","description":"Enables the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":["supports_multiple_windows"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"Denies the identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["identifier"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-data-store":{"identifier":"deny-remove-data-store","description":"Denies the remove_data_store command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_data_store"]}},"deny-remove-listener":{"identifier":"deny-remove-listener","description":"Denies the remove_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_listener"]}},"deny-set-app-theme":{"identifier":"deny-set-app-theme","description":"Denies the set_app_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_app_theme"]}},"deny-set-dock-visibility":{"identifier":"deny-set-dock-visibility","description":"Denies the set_dock_visibility command without any pre-configured scope.","commands":{"allow":[],"deny":["set_dock_visibility"]}},"deny-supports-multiple-windows":{"identifier":"deny-supports-multiple-windows","description":"Denies the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["supports_multiple_windows"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-icon-with-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-icon-with-as-template":{"identifier":"allow-set-icon-with-as-template","description":"Enables the set_icon_with_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_with_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-icon-with-as-template":{"identifier":"deny-set-icon-with-as-template","description":"Denies the set_icon_with_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_with_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-clear-all-browsing-data":{"identifier":"allow-clear-all-browsing-data","description":"Enables the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":["clear_all_browsing_data"],"deny":[]}},"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-auto-resize":{"identifier":"allow-set-webview-auto-resize","description":"Enables the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":["set_webview_auto_resize"],"deny":[]}},"allow-set-webview-background-color":{"identifier":"allow-set-webview-background-color","description":"Enables the set_webview_background_color command without any pre-configured scope.","commands":{"allow":["set_webview_background_color"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-hide":{"identifier":"allow-webview-hide","description":"Enables the webview_hide command without any pre-configured scope.","commands":{"allow":["webview_hide"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-show":{"identifier":"allow-webview-show","description":"Enables the webview_show command without any pre-configured scope.","commands":{"allow":["webview_show"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-clear-all-browsing-data":{"identifier":"deny-clear-all-browsing-data","description":"Denies the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":[],"deny":["clear_all_browsing_data"]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-auto-resize":{"identifier":"deny-set-webview-auto-resize","description":"Denies the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_auto_resize"]}},"deny-set-webview-background-color":{"identifier":"deny-set-webview-background-color","description":"Denies the set_webview_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_background_color"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-hide":{"identifier":"deny-webview-hide","description":"Denies the webview_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_hide"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-show":{"identifier":"deny-webview-show","description":"Denies the webview_show command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_show"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-is-enabled","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-is-always-on-top","allow-activity-name","allow-scene-identifier","allow-internal-toggle-maximize"]},"permissions":{"allow-activity-name":{"identifier":"allow-activity-name","description":"Enables the activity_name command without any pre-configured scope.","commands":{"allow":["activity_name"],"deny":[]}},"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-always-on-top":{"identifier":"allow-is-always-on-top","description":"Enables the is_always_on_top command without any pre-configured scope.","commands":{"allow":["is_always_on_top"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-scene-identifier":{"identifier":"allow-scene-identifier","description":"Enables the scene_identifier command without any pre-configured scope.","commands":{"allow":["scene_identifier"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-background-color":{"identifier":"allow-set-background-color","description":"Enables the set_background_color command without any pre-configured scope.","commands":{"allow":["set_background_color"],"deny":[]}},"allow-set-badge-count":{"identifier":"allow-set-badge-count","description":"Enables the set_badge_count command without any pre-configured scope.","commands":{"allow":["set_badge_count"],"deny":[]}},"allow-set-badge-label":{"identifier":"allow-set-badge-label","description":"Enables the set_badge_label command without any pre-configured scope.","commands":{"allow":["set_badge_label"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-focusable":{"identifier":"allow-set-focusable","description":"Enables the set_focusable command without any pre-configured scope.","commands":{"allow":["set_focusable"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-overlay-icon":{"identifier":"allow-set-overlay-icon","description":"Enables the set_overlay_icon command without any pre-configured scope.","commands":{"allow":["set_overlay_icon"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-simple-fullscreen":{"identifier":"allow-set-simple-fullscreen","description":"Enables the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":["set_simple_fullscreen"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-theme":{"identifier":"allow-set-theme","description":"Enables the set_theme command without any pre-configured scope.","commands":{"allow":["set_theme"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-activity-name":{"identifier":"deny-activity-name","description":"Denies the activity_name command without any pre-configured scope.","commands":{"allow":[],"deny":["activity_name"]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-always-on-top":{"identifier":"deny-is-always-on-top","description":"Denies the is_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["is_always_on_top"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-scene-identifier":{"identifier":"deny-scene-identifier","description":"Denies the scene_identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["scene_identifier"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-background-color":{"identifier":"deny-set-background-color","description":"Denies the set_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_background_color"]}},"deny-set-badge-count":{"identifier":"deny-set-badge-count","description":"Denies the set_badge_count command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_count"]}},"deny-set-badge-label":{"identifier":"deny-set-badge-label","description":"Denies the set_badge_label command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_label"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-focusable":{"identifier":"deny-set-focusable","description":"Denies the set_focusable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focusable"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-overlay-icon":{"identifier":"deny-set-overlay-icon","description":"Denies the set_overlay_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_overlay_icon"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-simple-fullscreen":{"identifier":"deny-set-simple-fullscreen","description":"Denies the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_simple_fullscreen"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-theme":{"identifier":"deny-set-theme","description":"Denies the set_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_theme"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null},"dialog":{"default_permission":{"identifier":"default","description":"This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n","permissions":["allow-message","allow-save","allow-open"]},"permissions":{"allow-ask":{"identifier":"allow-ask","description":"Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)","commands":{"allow":["message"],"deny":[]}},"allow-confirm":{"identifier":"allow-confirm","description":"Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)","commands":{"allow":["message"],"deny":[]}},"allow-message":{"identifier":"allow-message","description":"Enables the message command without any pre-configured scope.","commands":{"allow":["message"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-save":{"identifier":"allow-save","description":"Enables the save command without any pre-configured scope.","commands":{"allow":["save"],"deny":[]}},"deny-ask":{"identifier":"deny-ask","description":"Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)","commands":{"allow":[],"deny":["message"]}},"deny-confirm":{"identifier":"deny-confirm","description":"Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)","commands":{"allow":[],"deny":["message"]}},"deny-message":{"identifier":"deny-message","description":"Denies the message command without any pre-configured scope.","commands":{"allow":[],"deny":["message"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-save":{"identifier":"deny-save","description":"Denies the save command without any pre-configured scope.","commands":{"allow":[],"deny":["save"]}}},"permission_sets":{},"global_scope_schema":null}} \ No newline at end of file diff --git a/web/src-tauri/gen/schemas/capabilities.json b/web/src-tauri/gen/schemas/capabilities.json index 0ac735c..369d085 100644 --- a/web/src-tauri/gen/schemas/capabilities.json +++ b/web/src-tauri/gen/schemas/capabilities.json @@ -1 +1 @@ -{"main":{"identifier":"main","description":"Allow the local MediaProcessPipeline web UI to inspect and manage the desktop-owned backend process.","local":true,"windows":["main"],"webviews":["main"],"permissions":["core:default","allow-backend-get-status","allow-backend-get-logs","allow-backend-start","allow-backend-stop","allow-backend-restart","allow-backend-retry","allow-open-logs","allow-open-external-url","dialog:allow-open"]}} \ No newline at end of file +{"main":{"identifier":"main","description":"Allow the local MediaProcessPipeline web UI to inspect and manage the desktop-owned backend process.","local":true,"windows":["main"],"webviews":["main"],"permissions":["core:default","allow-backend-get-status","allow-bootstrap-get-preflight","allow-backend-get-logs","allow-backend-start","allow-backend-stop","allow-backend-restart","allow-backend-retry","allow-open-logs","allow-open-external-url","dialog:allow-open"]}} \ No newline at end of file diff --git a/web/src-tauri/gen/schemas/desktop-schema.json b/web/src-tauri/gen/schemas/desktop-schema.json index be0ef88..344973c 100644 --- a/web/src-tauri/gen/schemas/desktop-schema.json +++ b/web/src-tauri/gen/schemas/desktop-schema.json @@ -212,6 +212,12 @@ "const": "allow-backend-stop", "markdownDescription": "Allows stopping the desktop-managed backend process." }, + { + "description": "Allows reading the cached desktop bootstrap preflight report.", + "type": "string", + "const": "allow-bootstrap-get-preflight", + "markdownDescription": "Allows reading the cached desktop bootstrap preflight report." + }, { "description": "Allows opening http(s) source links in the system default browser.", "type": "string", diff --git a/web/src-tauri/gen/schemas/windows-schema.json b/web/src-tauri/gen/schemas/windows-schema.json index be0ef88..344973c 100644 --- a/web/src-tauri/gen/schemas/windows-schema.json +++ b/web/src-tauri/gen/schemas/windows-schema.json @@ -212,6 +212,12 @@ "const": "allow-backend-stop", "markdownDescription": "Allows stopping the desktop-managed backend process." }, + { + "description": "Allows reading the cached desktop bootstrap preflight report.", + "type": "string", + "const": "allow-bootstrap-get-preflight", + "markdownDescription": "Allows reading the cached desktop bootstrap preflight report." + }, { "description": "Allows opening http(s) source links in the system default browser.", "type": "string", diff --git a/web/src-tauri/permissions/backend.toml b/web/src-tauri/permissions/backend.toml index f40b1bf..79b75c1 100644 --- a/web/src-tauri/permissions/backend.toml +++ b/web/src-tauri/permissions/backend.toml @@ -3,6 +3,11 @@ identifier = "allow-backend-get-status" description = "Allows reading the desktop-managed backend process status." commands.allow = ["backend_get_status"] +[[permission]] +identifier = "allow-bootstrap-get-preflight" +description = "Allows reading the cached desktop bootstrap preflight report." +commands.allow = ["bootstrap_get_preflight"] + [[permission]] identifier = "allow-backend-get-logs" description = "Allows reading the desktop-managed backend process logs." diff --git a/web/src-tauri/src/main.rs b/web/src-tauri/src/main.rs index 9a3e920..942bf0e 100644 --- a/web/src-tauri/src/main.rs +++ b/web/src-tauri/src/main.rs @@ -4,7 +4,7 @@ use std::{ collections::{BTreeMap, BTreeSet}, env, error::Error, - ffi::OsString, + ffi::{OsStr, OsString}, fs, io::{self, Read, Write}, net::{IpAddr, Ipv4Addr, Ipv6Addr, Shutdown, SocketAddr, TcpListener, TcpStream}, @@ -49,6 +49,15 @@ const BACKEND_COMMAND: &str = "uv run python -u -m app.cli serve --desktop-loopb const MAX_LOG_LINES: usize = 1200; const MAX_LOG_LINE_BYTES: usize = 16 * 1024; const PROCESS_FORCE_TIMEOUT: Duration = Duration::from_secs(3); +const PREFLIGHT_PROCESS_TIMEOUT: Duration = Duration::from_secs(3); +const MAX_PREFLIGHT_OUTPUT_BYTES: usize = 16 * 1024; +const MAX_PREFLIGHT_DETAIL_BYTES: usize = 1024; +const MAX_PREFLIGHT_PATH_BYTES: usize = 2048; +const MAX_RUNTIME_SETTINGS_BYTES: u64 = 1024 * 1024; +const MAX_PATH_SEARCH_ENTRIES: usize = 256; +const PREFLIGHT_SCHEMA_VERSION: u32 = 1; +const SETTINGS_PREFLIGHT_OK_TOKEN: &str = "MPP_SETTINGS_PREFLIGHT_V1_OK"; +const SETTINGS_PREFLIGHT_INVALID_TOKEN: &str = "MPP_SETTINGS_PREFLIGHT_V1_INVALID"; const HEALTH_PRODUCT: &str = "com.mpp.backend"; const HEALTH_PROTOCOL: u32 = 1; const HEALTH_SERVICE: &str = "Media Process Pipeline"; @@ -367,6 +376,73 @@ struct BackendLogEntry { line: String, } +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +struct BootstrapPreflightReport { + schema_version: u32, + overall_status: String, + components: Vec, +} + +impl Default for BootstrapPreflightReport { + fn default() -> Self { + scanning_preflight_report() + } +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +struct BootstrapPreflightComponent { + component_id: String, + label: String, + status: String, + required: bool, + version: Option, + path: Option, + error_code: Option, + remediation: Option, + detail: Option, +} + +#[derive(Debug)] +struct ProbeCommandOutput { + success: bool, + output: String, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum SettingsPreflightOutcome { + Valid, + Invalid, + Error, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +enum PythonVersionOutcome { + Supported(String), + Unsupported(String), + Invalid, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum ProbeCommandFailureKind { + PermissionDenied, + Spawn, + Timeout, + Supervision, +} + +#[derive(Debug)] +struct ProbeCommandFailure { + kind: ProbeCommandFailureKind, + detail: String, +} + +#[derive(Debug)] +enum ProbeExecutableResolution { + Found(PathBuf), + Missing, + Invalid(PathBuf, String), +} + struct ProcessJob { handle: Option, } @@ -433,43 +509,43 @@ impl Default for BackendProcess { #[derive(Clone, Debug, Eq, PartialEq)] struct BootstrapFailure { retryable: bool, - error_code: &'static str, - component_id: &'static str, - remediation: &'static str, + error_code: String, + component_id: String, + remediation: String, detail: String, local_path: Option, } impl BootstrapFailure { fn retryable( - error_code: &'static str, - component_id: &'static str, - remediation: &'static str, + error_code: impl Into, + component_id: impl Into, + remediation: impl Into, detail: impl Into, local_path: Option<&Path>, ) -> Self { Self { retryable: true, - error_code, - component_id, - remediation, + error_code: error_code.into(), + component_id: component_id.into(), + remediation: remediation.into(), detail: detail.into(), local_path: local_path.map(|path| path.to_string_lossy().into_owned()), } } fn manual( - error_code: &'static str, - component_id: &'static str, - remediation: &'static str, + error_code: impl Into, + component_id: impl Into, + remediation: impl Into, detail: impl Into, local_path: Option<&Path>, ) -> Self { Self { retryable: false, - error_code, - component_id, - remediation, + error_code: error_code.into(), + component_id: component_id.into(), + remediation: remediation.into(), detail: detail.into(), local_path: local_path.map(|path| path.to_string_lossy().into_owned()), } @@ -492,6 +568,7 @@ struct BootstrapRuntime { attempt_epoch: u64, bootstrap_complete: bool, shutdown_requested: bool, + preflight: BootstrapPreflightReport, } struct BootstrapController { @@ -508,6 +585,217 @@ impl BootstrapController { } } +const PREFLIGHT_COMPONENT_ORDER: [&str; 10] = [ + "desktop-runtime", + "data-root", + "bundled-uv", + "python-environment", + "ffmpeg", + "ffprobe", + "desktop-proxy-port", + "backend-private-port", + "runtime-settings", + "webview2", +]; + +fn canonical_preflight_component_id(component_id: &str) -> &str { + match component_id { + "python-runtime" => "python-environment", + "backend-health" => "python-environment", + "desktop-proxy" => "desktop-proxy-port", + "backend-port" => "backend-private-port", + "desktop-webview" => "webview2", + "desktop-bootstrap" | "desktop-session" => "desktop-runtime", + value if PREFLIGHT_COMPONENT_ORDER.contains(&value) => value, + _ => "desktop-runtime", + } +} + +fn preflight_component_label(component_id: &str) -> &str { + match component_id { + "desktop-runtime" => "Desktop Runtime", + "data-root" => "Local Data Root", + "bundled-uv" => "uv Runtime Manager", + "python-environment" => "Python Environment", + "ffmpeg" => "FFmpeg", + "ffprobe" => "FFprobe", + "desktop-proxy-port" => "Desktop Proxy Port", + "backend-private-port" => "Backend Private Port", + "runtime-settings" => "Runtime Settings", + "webview2" => "Microsoft Edge WebView2", + _ => "Desktop Bootstrap", + } +} + +fn bounded_preflight_text(value: impl AsRef, maximum_bytes: usize) -> String { + let value = value.as_ref(); + if value.len() <= maximum_bytes { + return value.to_string(); + } + let suffix = "..."; + let content_limit = maximum_bytes.saturating_sub(suffix.len()); + let mut boundary = content_limit.min(value.len()); + while boundary > 0 && !value.is_char_boundary(boundary) { + boundary -= 1; + } + format!("{}{}", &value[..boundary], suffix) +} + +fn bounded_preflight_path(path: &Path) -> String { + bounded_preflight_text(path.to_string_lossy(), MAX_PREFLIGHT_PATH_BYTES) +} + +fn preflight_component( + component_id: &str, + label: &str, + status: &str, + required: bool, + version: Option, + path: Option<&Path>, + error_code: Option<&str>, + remediation: Option<&str>, + detail: Option, +) -> BootstrapPreflightComponent { + BootstrapPreflightComponent { + component_id: component_id.to_string(), + label: label.to_string(), + status: status.to_string(), + required, + version: version.map(|value| bounded_preflight_text(value, 128)), + path: path.map(bounded_preflight_path), + error_code: error_code.map(str::to_string), + remediation: remediation + .map(|value| bounded_preflight_text(value, MAX_PREFLIGHT_DETAIL_BYTES)), + detail: detail + .filter(|value| !value.is_empty()) + .map(|value| bounded_preflight_text(value, MAX_PREFLIGHT_DETAIL_BYTES)), + } +} + +fn scanning_preflight_report() -> BootstrapPreflightReport { + BootstrapPreflightReport { + schema_version: PREFLIGHT_SCHEMA_VERSION, + overall_status: "scanning".to_string(), + components: PREFLIGHT_COMPONENT_ORDER + .iter() + .map(|component_id| { + preflight_component( + component_id, + preflight_component_label(component_id), + "scanning", + true, + None, + None, + None, + None, + None, + ) + }) + .collect(), + } +} + +fn preflight_component_rank(component_id: &str) -> (usize, &str) { + ( + PREFLIGHT_COMPONENT_ORDER + .iter() + .position(|candidate| *candidate == component_id) + .unwrap_or(PREFLIGHT_COMPONENT_ORDER.len()), + component_id, + ) +} + +fn finalize_preflight_report( + mut components: Vec, +) -> BootstrapPreflightReport { + components.sort_by(|left, right| { + preflight_component_rank(&left.component_id) + .cmp(&preflight_component_rank(&right.component_id)) + }); + let overall_status = if components + .iter() + .any(|component| component.required && component.status == "blocked") + { + "blocked" + } else if components.iter().any(|component| { + component.required && matches!(component.status.as_str(), "missing" | "invalid") + }) { + "needs_repair" + } else if components + .iter() + .any(|component| component.status != "ready") + { + "needs_configuration" + } else { + "ready" + }; + BootstrapPreflightReport { + schema_version: PREFLIGHT_SCHEMA_VERSION, + overall_status: overall_status.to_string(), + components, + } +} + +fn preflight_blocking_failure(report: &BootstrapPreflightReport) -> Option { + let component = report.components.iter().find(|component| { + component.required && matches!(component.status.as_str(), "missing" | "invalid" | "blocked") + })?; + let retryable = component.status == "blocked" + || matches!( + component.component_id.as_str(), + "data-root" | "desktop-proxy-port" | "backend-private-port" + ); + Some(BootstrapFailure { + retryable, + error_code: component + .error_code + .clone() + .unwrap_or_else(|| "PREFLIGHT_REQUIRED_COMPONENT_FAILED".to_string()), + component_id: component.component_id.clone(), + remediation: component + .remediation + .clone() + .unwrap_or_else(|| "Repair the reported desktop component and retry.".to_string()), + detail: component + .detail + .clone() + .unwrap_or_else(|| format!("{} did not pass preflight.", component.label)), + local_path: component.path.clone(), + }) +} + +fn apply_failure_to_preflight(report: &mut BootstrapPreflightReport, failure: &BootstrapFailure) { + let component_id = canonical_preflight_component_id(&failure.component_id); + let failure_status = if failure.error_code.ends_with("_MISSING") { + "missing" + } else if failure.retryable { + "blocked" + } else { + "invalid" + }; + if let Some(component) = report + .components + .iter_mut() + .find(|component| component.component_id == component_id) + { + component.status = failure_status.to_string(); + component.required = true; + component.error_code = Some(failure.error_code.clone()); + component.remediation = Some(bounded_preflight_text( + &failure.remediation, + MAX_PREFLIGHT_DETAIL_BYTES, + )); + component.detail = Some(bounded_preflight_text( + &failure.detail, + MAX_PREFLIGHT_DETAIL_BYTES, + )); + if let Some(path) = failure.local_path.as_deref() { + component.path = Some(bounded_preflight_text(path, MAX_PREFLIGHT_PATH_BYTES)); + } + } + *report = finalize_preflight_report(std::mem::take(&mut report.components)); +} + fn boxed_error(message: impl Into) -> Box { Box::new(std::io::Error::new( std::io::ErrorKind::Other, @@ -1173,6 +1461,34 @@ fn runtime_layout_from_parts( } } +fn runtime_resolution_failure(error: String, runtime_hint: Option<&Path>) -> BootstrapFailure { + let lower = error.to_ascii_lowercase(); + let uv_related = lower.contains("bin/uv.exe") + || lower.contains("bundled uv") + || lower.contains("runtime uv metadata") + || lower.contains("uv pe ") + || lower.contains("uv dos ") + || lower.contains("/tools/uv/"); + if uv_related { + let missing = lower.contains("missing") || lower.contains("absent"); + let uv_path = runtime_hint.map(|path| path.join("bin").join("uv.exe")); + return BootstrapFailure::manual( + if missing { "UV_MISSING" } else { "UV_INVALID" }, + "bundled-uv", + "Repair or reinstall the bundled uv runtime, then retry.", + error, + uv_path.as_deref(), + ); + } + BootstrapFailure::manual( + "RUNTIME_INVALID", + "desktop-runtime", + "Repair or reinstall MediaProcessPipeline, then retry.", + error, + runtime_hint, + ) +} + fn resolve_runtime_layout(app: &AppHandle) -> Result { let policy = RuntimeResolutionPolicy::current_build(); let explicit_root = env::var_os("MPP_PROJECT_ROOT") @@ -1215,15 +1531,8 @@ fn resolve_runtime_layout(app: &AppHandle) -> Result Result<(), String> { use std::mem::size_of; @@ -3317,6 +3640,188 @@ fn close_job_handle(handle: usize) { #[cfg(not(windows))] fn close_job_handle(_handle: usize) {} +#[cfg(unix)] +fn terminate_probe_process_group(process_id: u32) -> bool { + const SIGKILL: i32 = 9; + unsafe extern "C" { + fn kill(process_id: i32, signal: i32) -> i32; + } + let Ok(process_id) = i32::try_from(process_id) else { + return false; + }; + unsafe { kill(-process_id, SIGKILL) == 0 } +} + +#[cfg(not(unix))] +fn terminate_probe_process_group(_process_id: u32) -> bool { + false +} + +fn terminate_probe_process_tree(job: &mut ProcessJob, child: &mut Child) { + if job.terminate_tree() || terminate_probe_process_group(child.id()) { + return; + } + let _ = child.kill(); +} + +fn read_bounded_probe_stream(mut reader: R) -> io::Result<(Vec, bool)> { + let mut retained = Vec::with_capacity(MAX_PREFLIGHT_OUTPUT_BYTES); + let mut chunk = [0_u8; 4096]; + let mut truncated = false; + loop { + let count = reader.read(&mut chunk)?; + if count == 0 { + break; + } + let available = MAX_PREFLIGHT_OUTPUT_BYTES.saturating_sub(retained.len()); + let copied = available.min(count); + retained.extend_from_slice(&chunk[..copied]); + truncated |= copied < count; + } + Ok((retained, truncated)) +} + +fn collect_probe_reader( + reader: thread::JoinHandle, bool)>>, + source: &str, +) -> Result<(Vec, bool), ProbeCommandFailure> { + reader + .join() + .map_err(|_| ProbeCommandFailure { + kind: ProbeCommandFailureKind::Supervision, + detail: format!("{source} reader panicked"), + })? + .map_err(|error| ProbeCommandFailure { + kind: ProbeCommandFailureKind::Supervision, + detail: format!("failed to read probe {source}: {error}"), + }) +} + +fn run_bounded_probe_command( + program: &Path, + arguments: &[&str], + timeout: Duration, +) -> Result { + let mut command = Command::new(program); + command + .env_clear() + .envs(safe_inherited_environment(env::vars_os())) + .args(arguments) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + configure_probe_process_creation(&mut command); + + let mut child = command.spawn().map_err(|error| ProbeCommandFailure { + kind: if error.kind() == io::ErrorKind::PermissionDenied { + ProbeCommandFailureKind::PermissionDenied + } else { + ProbeCommandFailureKind::Spawn + }, + detail: format!( + "failed to start {}: {error}", + bounded_preflight_path(program) + ), + })?; + let mut job = match attach_kill_on_close_job(&child) { + Ok(job) => job, + Err(error) => { + let _ = child.kill(); + let _ = wait_for_child_exit_until(&mut child, Instant::now() + PROCESS_FORCE_TIMEOUT); + return Err(ProbeCommandFailure { + kind: ProbeCommandFailureKind::Supervision, + detail: bounded_preflight_text(error, MAX_PREFLIGHT_DETAIL_BYTES), + }); + } + }; + if let Err(error) = resume_suspended_process(&child) { + terminate_probe_process_tree(&mut job, &mut child); + let _ = wait_for_child_exit_until(&mut child, Instant::now() + PROCESS_FORCE_TIMEOUT); + return Err(ProbeCommandFailure { + kind: ProbeCommandFailureKind::Supervision, + detail: bounded_preflight_text(error, MAX_PREFLIGHT_DETAIL_BYTES), + }); + } + + let stdout = child + .stdout + .take() + .expect("probe stdout was configured as piped"); + let stderr = child + .stderr + .take() + .expect("probe stderr was configured as piped"); + let stdout_reader = thread::spawn(move || read_bounded_probe_stream(stdout)); + let stderr_reader = thread::spawn(move || read_bounded_probe_stream(stderr)); + let deadline = Instant::now() + timeout; + let mut exit_status = None; + let mut wait_failure = None; + loop { + match child.try_wait() { + Ok(Some(status)) => { + exit_status = Some(status); + break; + } + Ok(None) if Instant::now() >= deadline => break, + Ok(None) => thread::sleep( + deadline + .saturating_duration_since(Instant::now()) + .min(Duration::from_millis(20)), + ), + Err(error) => { + wait_failure = Some(error); + break; + } + } + } + + let timed_out = exit_status.is_none() && wait_failure.is_none(); + terminate_probe_process_tree(&mut job, &mut child); + if exit_status.is_none() { + let _ = wait_for_child_exit_until(&mut child, Instant::now() + PROCESS_FORCE_TIMEOUT); + } + let (stdout, stdout_truncated) = collect_probe_reader(stdout_reader, "stdout")?; + let (stderr, stderr_truncated) = collect_probe_reader(stderr_reader, "stderr")?; + + if let Some(error) = wait_failure { + return Err(ProbeCommandFailure { + kind: ProbeCommandFailureKind::Supervision, + detail: format!("failed to inspect probe process: {error}"), + }); + } + if timed_out { + return Err(ProbeCommandFailure { + kind: ProbeCommandFailureKind::Timeout, + detail: format!( + "{} exceeded its {} ms probe timeout and was terminated", + bounded_preflight_path(program), + timeout.as_millis() + ), + }); + } + + let mut combined = stdout; + if !combined.is_empty() && !stderr.is_empty() { + combined.push(b'\n'); + } + let remaining = MAX_PREFLIGHT_OUTPUT_BYTES.saturating_sub(combined.len()); + combined.extend_from_slice(&stderr[..remaining.min(stderr.len())]); + let output_was_truncated = stdout_truncated || stderr_truncated || stderr.len() > remaining; + let mut output = String::from_utf8_lossy(&combined).trim().to_string(); + if output_was_truncated { + output = format!( + "{}...", + bounded_preflight_text(output, MAX_PREFLIGHT_OUTPUT_BYTES.saturating_sub(3)) + ); + } + Ok(ProbeCommandOutput { + success: exit_status + .expect("completed probe must have an exit status") + .success(), + output: bounded_preflight_text(output, MAX_PREFLIGHT_OUTPUT_BYTES), + }) +} + fn wait_for_child_exit_until( child: &mut Child, deadline: Instant, @@ -4058,6 +4563,16 @@ fn backend_get_status(app: AppHandle) -> Result { current_status(&app) } +#[tauri::command] +fn bootstrap_get_preflight(app: AppHandle) -> Result { + let controller = app.state::(); + controller + .runtime + .lock() + .map(|runtime| runtime.preflight.clone()) + .map_err(|_| "bootstrap runtime lock poisoned".to_string()) +} + #[tauri::command] fn backend_get_logs(app: AppHandle) -> Result, String> { let backend = app.state::(); @@ -4136,89 +4651,1184 @@ fn backend_restart(app: AppHandle) -> Result { Ok(status) } -fn bootstrap_internal_failure(error: impl Into) -> BootstrapFailure { - BootstrapFailure::manual( - "DESKTOP_BOOTSTRAP_FAILED", - "desktop-bootstrap", - "Open diagnostics, restart the application, and retry.", - error, - None, - ) +fn inspect_real_file(path: &Path, label: &str) -> Result<(), String> { + validate_existing_path_chain(path, label)?; + let metadata = fs::symlink_metadata(path) + .map_err(|error| format!("cannot inspect {}: {error}", path.display()))?; + if metadata_is_link_or_reparse(&metadata) || !metadata.is_file() { + return Err(format!("{label} must be a real file: {}", path.display())); + } + Ok(()) } -fn bootstrap_wait_failure(error: BackendWaitFailure, layout: &RuntimeLayout) -> BootstrapFailure { - let local_path = Some(layout.backend_dir.as_path()); - match error { - BackendWaitFailure::ProcessExited(detail) => BootstrapFailure::retryable( - "BACKEND_EXITED", - "python-runtime", - "Open the logs, correct the reported runtime or configuration error, and retry.", - detail, - local_path, - ), - BackendWaitFailure::Incompatible(detail) => BootstrapFailure::manual( - "BACKEND_HEALTH_INVALID", - "backend-health", - "Repair or reinstall the matching desktop runtime, then retry.", - detail, - local_path, - ), - BackendWaitFailure::Occupied(detail) => BootstrapFailure::retryable( - "PRIVATE_PORT_IN_USE", - "backend-port", - "Close the conflicting local process and retry.", - detail, - None, - ), - BackendWaitFailure::Timeout => BootstrapFailure::retryable( - "BACKEND_START_TIMEOUT", - "backend-health", - "Open the logs, check runtime initialization and network access, then retry.", - BackendWaitFailure::Timeout.to_string(), - local_path, - ), - BackendWaitFailure::Internal(detail) => bootstrap_internal_failure(detail), - } +fn inspect_probe_executable(path: &Path) -> Result<(), String> { + inspect_real_file(path, "preflight executable") } -fn prepare_bootstrap_runtime(app: &AppHandle) -> Result { - let controller = app.state::(); - if let Some(layout) = controller - .runtime - .lock() - .map_err(|_| bootstrap_internal_failure("bootstrap runtime lock poisoned"))? - .layout - .clone() +fn executable_candidate_names(name: &OsStr) -> Vec { + let path = Path::new(name); + if path.extension().is_some() { + return vec![name.to_os_string()]; + } + #[cfg(windows)] { - return Ok(layout); + let mut executable = name.to_os_string(); + executable.push(".exe"); + vec![executable, name.to_os_string()] } - - let layout = resolve_runtime_layout(app)?; - let mut runtime = controller - .runtime - .lock() - .map_err(|_| bootstrap_internal_failure("bootstrap runtime lock poisoned"))?; - if runtime.layout.is_none() { - runtime.layout = Some(layout.clone()); + #[cfg(not(windows))] + { + vec![name.to_os_string()] } - Ok(runtime.layout.clone().unwrap_or(layout)) } -fn ensure_bootstrap_proxy( - app: &AppHandle, - layout: &RuntimeLayout, - attempt_epoch: u64, -) -> Result { - let controller = app.state::(); - let mut runtime = controller - .runtime - .lock() - .map_err(|_| bootstrap_internal_failure("bootstrap runtime lock poisoned"))?; - if !bootstrap_attempt_is_current(&runtime, attempt_epoch) { - return Ok(false); +fn resolve_probe_executable( + configured: &OsStr, + preferred_directories: &[PathBuf], +) -> ProbeExecutableResolution { + let configured_path = Path::new(configured); + if configured_path.is_absolute() || configured_path.components().count() > 1 { + if !configured_path.is_absolute() { + return ProbeExecutableResolution::Invalid( + configured_path.to_path_buf(), + "configured probe executable path must be absolute".to_string(), + ); + } + match fs::symlink_metadata(configured_path) { + Err(error) if error.kind() == io::ErrorKind::NotFound => { + return ProbeExecutableResolution::Missing; + } + Err(error) => { + return ProbeExecutableResolution::Invalid( + configured_path.to_path_buf(), + format!("cannot inspect {}: {error}", configured_path.display()), + ); + } + Ok(_) => {} + } + return match inspect_probe_executable(configured_path) { + Ok(()) => ProbeExecutableResolution::Found(configured_path.to_path_buf()), + Err(error) => ProbeExecutableResolution::Invalid(configured_path.to_path_buf(), error), + }; } - if runtime.proxy_started { - return Ok(true); + + let candidate_names = executable_candidate_names(configured); + for directory in preferred_directories { + if !directory.is_absolute() { + continue; + } + for name in &candidate_names { + let candidate = directory.join(name); + match fs::symlink_metadata(&candidate) { + Err(error) if error.kind() == io::ErrorKind::NotFound => continue, + Err(error) => { + return ProbeExecutableResolution::Invalid( + candidate.clone(), + format!("cannot inspect {}: {error}", candidate.display()), + ); + } + Ok(_) => {} + } + return match inspect_probe_executable(&candidate) { + Ok(()) => ProbeExecutableResolution::Found(candidate), + Err(error) => ProbeExecutableResolution::Invalid(candidate, error), + }; + } + } + + let Some(search_path) = env::var_os("PATH") else { + return ProbeExecutableResolution::Missing; + }; + for directory in env::split_paths(&search_path).take(MAX_PATH_SEARCH_ENTRIES) { + if !directory.is_absolute() { + continue; + } + for name in &candidate_names { + let candidate = directory.join(name); + match fs::symlink_metadata(&candidate) { + Err(error) if error.kind() == io::ErrorKind::NotFound => continue, + Err(error) => { + return ProbeExecutableResolution::Invalid( + candidate.clone(), + format!("cannot inspect {}: {error}", candidate.display()), + ); + } + Ok(_) => {} + } + return match inspect_probe_executable(&candidate) { + Ok(()) => ProbeExecutableResolution::Found(candidate), + Err(error) => ProbeExecutableResolution::Invalid(candidate, error), + }; + } + } + ProbeExecutableResolution::Missing +} + +fn first_probe_output_line(output: &str) -> Option { + output + .lines() + .map(str::trim) + .find(|line| !line.is_empty()) + .map(|line| bounded_preflight_text(line, 256)) +} + +fn classify_python_version_output(output: &ProbeCommandOutput) -> PythonVersionOutcome { + if !output.success { + return PythonVersionOutcome::Invalid; + } + let Some(line) = first_probe_output_line(&output.output) else { + return PythonVersionOutcome::Invalid; + }; + let mut fields = line.split_whitespace(); + if fields.next() != Some("Python") { + return PythonVersionOutcome::Invalid; + } + let Some(version) = fields.next() else { + return PythonVersionOutcome::Invalid; + }; + if fields.next().is_some() { + return PythonVersionOutcome::Invalid; + } + let parts = version.split('.').collect::>(); + if parts.len() != 3 || parts.iter().any(|part| part.parse::().is_err()) { + return PythonVersionOutcome::Invalid; + } + let parsed = parts + .iter() + .map(|part| part.parse::().expect("numeric Python version part")) + .collect::>(); + if parsed[0] == 3 && matches!(parsed[1], 11 | 12) { + PythonVersionOutcome::Supported(version.to_string()) + } else { + PythonVersionOutcome::Unsupported(version.to_string()) + } +} + +fn validated_media_tool_version_line(tool: &str, output: &ProbeCommandOutput) -> Option { + if !output.success { + return None; + } + let line = first_probe_output_line(&output.output)?; + let prefix = format!("{tool} version "); + let version = line.strip_prefix(&prefix)?.split_whitespace().next()?; + if version.is_empty() { + return None; + } + Some(line) +} + +fn trusted_uv_version() -> Result { + let contract: serde_json::Value = serde_json::from_str(TRUSTED_TOOL_CONTRACT_JSON) + .map_err(|error| format!("compiled desktop tool contract is invalid: {error}"))?; + json_string(&contract, "/tools/uv/version").map(str::to_string) +} + +fn probe_uv_component(layout: &RuntimeLayout) -> BootstrapPreflightComponent { + let required = layout.mode == RuntimeMode::Installed; + let resolution = resolve_probe_executable(&layout.uv_executable, &[]); + let executable = match resolution { + ProbeExecutableResolution::Found(path) => path, + ProbeExecutableResolution::Missing => { + return preflight_component( + "bundled-uv", + "uv Runtime Manager", + "missing", + required, + None, + None, + Some("UV_MISSING"), + Some("Repair the desktop runtime or configure an available uv executable."), + Some("The uv executable could not be resolved.".to_string()), + ); + } + ProbeExecutableResolution::Invalid(path, detail) => { + return preflight_component( + "bundled-uv", + "uv Runtime Manager", + "invalid", + required, + None, + Some(&path), + Some("UV_INVALID"), + Some("Repair the uv executable and retry."), + Some(detail), + ); + } + }; + + let output = + match run_bounded_probe_command(&executable, &["--version"], PREFLIGHT_PROCESS_TIMEOUT) { + Ok(output) => output, + Err(failure) => { + let (status, error_code, remediation) = match failure.kind { + ProbeCommandFailureKind::Timeout => ( + "blocked", + "UV_PROBE_TIMEOUT", + "Close stalled uv processes, repair the runtime if needed, and retry.", + ), + ProbeCommandFailureKind::PermissionDenied => ( + "blocked", + "UV_EXECUTION_BLOCKED", + "Restore permission to run the uv executable and retry.", + ), + ProbeCommandFailureKind::Supervision => ( + "blocked", + "UV_PROBE_BLOCKED", + "Restart the application and retry the supervised uv check.", + ), + ProbeCommandFailureKind::Spawn => ( + "invalid", + "UV_INVALID", + "Repair the uv executable and retry.", + ), + }; + return preflight_component( + "bundled-uv", + "uv Runtime Manager", + status, + required, + None, + Some(&executable), + Some(error_code), + Some(remediation), + Some(failure.detail), + ); + } + }; + let Some(line) = first_probe_output_line(&output.output) else { + return preflight_component( + "bundled-uv", + "uv Runtime Manager", + "invalid", + required, + None, + Some(&executable), + Some("UV_INVALID"), + Some("Repair the uv executable and retry."), + Some("uv --version returned no version output.".to_string()), + ); + }; + let actual_version = line + .strip_prefix("uv ") + .and_then(|value| value.split_whitespace().next()) + .map(str::to_string); + if !output.success || actual_version.is_none() { + return preflight_component( + "bundled-uv", + "uv Runtime Manager", + "invalid", + required, + actual_version, + Some(&executable), + Some("UV_INVALID"), + Some("Repair the uv executable and retry."), + Some(format!("uv version probe failed: {line}")), + ); + } + let actual_version = actual_version.expect("uv version was checked"); + let expected_version = match trusted_uv_version() { + Ok(version) => version, + Err(error) => { + return preflight_component( + "bundled-uv", + "uv Runtime Manager", + "invalid", + required, + Some(actual_version), + Some(&executable), + Some("UV_CONTRACT_INVALID"), + Some("Repair or reinstall the desktop application."), + Some(error), + ); + } + }; + if actual_version != expected_version { + return preflight_component( + "bundled-uv", + "uv Runtime Manager", + if required { "invalid" } else { "warning" }, + required, + Some(actual_version.clone()), + Some(&executable), + Some("UV_VERSION_MISMATCH"), + Some("Install the desktop-supported uv version or repair the bundled runtime."), + Some(format!( + "uv version {actual_version} does not match required version {expected_version}." + )), + ); + } + preflight_component( + "bundled-uv", + "uv Runtime Manager", + "ready", + required, + Some(actual_version), + Some(&executable), + None, + None, + Some("The uv executable and version passed the bounded process probe.".to_string()), + ) +} + +fn tool_probe_failure_mapping( + tool: &str, + failure_kind: ProbeCommandFailureKind, +) -> (&'static str, &'static str, &'static str) { + match (tool, failure_kind) { + ("ffmpeg", ProbeCommandFailureKind::Timeout) => ( + "blocked", + "FFMPEG_PROBE_TIMEOUT", + "Close stalled FFmpeg processes and retry.", + ), + ("ffmpeg", ProbeCommandFailureKind::PermissionDenied) => ( + "blocked", + "FFMPEG_EXECUTION_BLOCKED", + "Restore permission to run FFmpeg and retry.", + ), + ("ffmpeg", ProbeCommandFailureKind::Supervision) => ( + "blocked", + "FFMPEG_PROBE_BLOCKED", + "Restart the application and retry the supervised FFmpeg check.", + ), + ("ffprobe", ProbeCommandFailureKind::Timeout) => ( + "blocked", + "FFPROBE_PROBE_TIMEOUT", + "Close stalled FFprobe processes and retry.", + ), + ("ffprobe", ProbeCommandFailureKind::PermissionDenied) => ( + "blocked", + "FFPROBE_EXECUTION_BLOCKED", + "Restore permission to run FFprobe and retry.", + ), + ("ffprobe", ProbeCommandFailureKind::Supervision) => ( + "blocked", + "FFPROBE_PROBE_BLOCKED", + "Restart the application and retry the supervised FFprobe check.", + ), + ("ffprobe", ProbeCommandFailureKind::Spawn) => ( + "invalid", + "FFPROBE_INVALID", + "Repair the FFprobe executable and retry.", + ), + _ => ( + "invalid", + "FFMPEG_INVALID", + "Repair the FFmpeg executable and retry.", + ), + } +} + +fn python_probe_failure_mapping( + failure_kind: ProbeCommandFailureKind, +) -> (&'static str, &'static str, &'static str) { + match failure_kind { + ProbeCommandFailureKind::Timeout => ( + "blocked", + "PYTHON_PROBE_TIMEOUT", + "Close stalled Python processes and retry.", + ), + ProbeCommandFailureKind::PermissionDenied => ( + "blocked", + "PYTHON_EXECUTION_BLOCKED", + "Restore permission to run the desktop Python environment and retry.", + ), + ProbeCommandFailureKind::Supervision => ( + "blocked", + "PYTHON_PROBE_BLOCKED", + "Restart the application and retry the supervised Python check.", + ), + ProbeCommandFailureKind::Spawn => ( + "invalid", + "PYTHON_ENVIRONMENT_INVALID", + "Repair or provision the desktop Python environment and retry.", + ), + } +} + +fn probe_media_tool_component( + layout: &RuntimeLayout, + tool: &'static str, + label: &'static str, +) -> BootstrapPreflightComponent { + let required = layout.mode == RuntimeMode::Installed; + let preferred = [layout.runtime_root.join("bin")]; + let resolution = resolve_probe_executable(OsStr::new(tool), &preferred); + let (missing_code, invalid_code) = if tool == "ffprobe" { + ("FFPROBE_MISSING", "FFPROBE_INVALID") + } else { + ("FFMPEG_MISSING", "FFMPEG_INVALID") + }; + let executable = match resolution { + ProbeExecutableResolution::Found(path) => path, + ProbeExecutableResolution::Missing => { + return preflight_component( + tool, + label, + "missing", + required, + None, + None, + Some(missing_code), + Some("Install FFmpeg with both ffmpeg and ffprobe available, then retry."), + Some(format!( + "{label} could not be resolved from the runtime or PATH." + )), + ); + } + ProbeExecutableResolution::Invalid(path, detail) => { + return preflight_component( + tool, + label, + "invalid", + required, + None, + Some(&path), + Some(invalid_code), + Some("Repair the FFmpeg installation and retry."), + Some(detail), + ); + } + }; + let output = + match run_bounded_probe_command(&executable, &["-version"], PREFLIGHT_PROCESS_TIMEOUT) { + Ok(output) => output, + Err(failure) => { + let (status, error_code, remediation) = + tool_probe_failure_mapping(tool, failure.kind); + return preflight_component( + tool, + label, + status, + required, + None, + Some(&executable), + Some(error_code), + Some(remediation), + Some(failure.detail), + ); + } + }; + let version_line = validated_media_tool_version_line(tool, &output); + if version_line.is_none() { + return preflight_component( + tool, + label, + "invalid", + required, + None, + Some(&executable), + Some(invalid_code), + Some("Repair the FFmpeg installation and retry."), + Some(format!( + "{label} version probe did not return the expected fixed version signature." + )), + ); + } + preflight_component( + tool, + label, + "ready", + required, + version_line, + Some(&executable), + None, + None, + Some(format!("{label} passed the bounded process probe.")), + ) +} + +fn inspect_real_directory(path: &Path, label: &str) -> Result<(), String> { + validate_existing_path_chain(path, label)?; + let metadata = fs::symlink_metadata(path) + .map_err(|error| format!("cannot inspect {}: {error}", path.display()))?; + if metadata_is_link_or_reparse(&metadata) || !metadata.is_dir() { + return Err(format!( + "{label} must be a real directory: {}", + path.display() + )); + } + Ok(()) +} + +fn probe_directory_writable(path: &Path, index: usize) -> Result<(), String> { + let probe_path = path.join(format!( + ".preflight-write-{}-{}-{index}", + std::process::id(), + Utc::now().timestamp_micros() + )); + let probe_result = (|| -> io::Result<()> { + let mut probe = fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&probe_path)?; + probe.write_all(b"mpp-preflight")?; + probe.sync_all() + })(); + if let Err(error) = probe_result { + let _ = fs::remove_file(&probe_path); + return Err(format!( + "desktop data directory is not writable at {}: {error}", + path.display() + )); + } + fs::remove_file(&probe_path).map_err(|error| { + format!( + "desktop data write probe could not be removed from {}: {error}", + path.display() + ) + }) +} + +fn read_runtime_version(runtime_root: &Path) -> Result { + let path = runtime_root.join("VERSION"); + let metadata = fs::symlink_metadata(&path) + .map_err(|error| format!("cannot inspect runtime VERSION: {error}"))?; + if metadata_is_link_or_reparse(&metadata) || !metadata.is_file() || metadata.len() > 4096 { + return Err("runtime VERSION must be a small regular file".to_string()); + } + let mut bytes = Vec::with_capacity((metadata.len() as usize).saturating_add(1)); + fs::File::open(&path) + .map_err(|error| format!("cannot open runtime VERSION: {error}"))? + .take(4097) + .read_to_end(&mut bytes) + .map_err(|error| format!("cannot read runtime VERSION: {error}"))?; + if bytes.len() > 4096 { + return Err("runtime VERSION changed beyond the 4096 byte limit".to_string()); + } + let version = std::str::from_utf8(&bytes) + .map_err(|error| format!("runtime VERSION is not UTF-8: {error}"))?; + let version = version.trim(); + if version.is_empty() { + return Err("runtime VERSION is empty".to_string()); + } + Ok(bounded_preflight_text(version, 128)) +} + +fn probe_desktop_runtime_component(layout: &RuntimeLayout) -> BootstrapPreflightComponent { + let required = layout.mode == RuntimeMode::Installed; + let inspection = if required { + validate_installed_runtime_manifest(&layout.runtime_root) + .map_err(|error| format!("installed runtime validation failed: {error}")) + } else { + validate_runtime_root(&layout.runtime_root, false) + } + .and_then(|_| inspect_real_directory(&layout.runtime_root, "desktop runtime")) + .and_then(|_| inspect_real_directory(&layout.backend_dir, "backend runtime")) + .and_then(|_| inspect_real_directory(&layout.web_dist_dir, "web runtime")) + .and_then(|_| read_runtime_version(&layout.runtime_root)); + match inspection { + Ok(version) => preflight_component( + "desktop-runtime", + "Desktop Runtime", + "ready", + required, + Some(version), + Some(&layout.runtime_root), + None, + None, + Some("The resolved runtime layout is complete.".to_string()), + ), + Err(detail) => preflight_component( + "desktop-runtime", + "Desktop Runtime", + if required { "invalid" } else { "warning" }, + required, + None, + Some(&layout.runtime_root), + Some("RUNTIME_INVALID"), + Some("Repair or reinstall MediaProcessPipeline, then retry."), + Some(detail), + ), + } +} + +fn probe_data_root_component(layout: &RuntimeLayout) -> BootstrapPreflightComponent { + let required = layout.mode == RuntimeMode::Installed; + let inspection = + inspect_real_directory(&layout.user.root, "desktop data root").and_then(|_| { + for (index, directory) in std::iter::once(layout.user.root.as_path()) + .chain(layout.user.required_directories()) + .enumerate() + { + inspect_real_directory(directory, "desktop data directory")?; + probe_directory_writable(directory, index)?; + } + Ok(()) + }); + match inspection { + Ok(()) => preflight_component( + "data-root", + "Local Data Root", + "ready", + required, + None, + Some(&layout.user.root), + None, + None, + Some("The local application directories are available.".to_string()), + ), + Err(detail) => preflight_component( + "data-root", + "Local Data Root", + if required { "blocked" } else { "warning" }, + required, + None, + Some(&layout.user.root), + Some("DATA_ROOT_UNWRITABLE"), + Some("Restore access to the local application data directory and retry."), + Some(detail), + ), + } +} + +fn python_venv_executable(venv_dir: &Path) -> PathBuf { + #[cfg(windows)] + { + venv_dir.join("Scripts").join("python.exe") + } + #[cfg(not(windows))] + { + venv_dir.join("bin").join("python") + } +} + +fn probe_python_environment_component(layout: &RuntimeLayout) -> BootstrapPreflightComponent { + let required = layout.mode == RuntimeMode::Installed; + let python = python_venv_executable(&layout.user.venv_dir); + if !layout.user.venv_dir.exists() || !python.exists() { + return preflight_component( + "python-environment", + "Python Environment", + if required { "missing" } else { "warning" }, + required, + None, + Some(&layout.user.venv_dir), + Some("PYTHON_ENVIRONMENT_MISSING"), + Some("Provision the desktop Python environment and retry."), + Some( + "The configured user virtual environment is absent; preflight does not create or synchronize it." + .to_string(), + ), + ); + } + let inspection = inspect_real_directory(&layout.user.venv_dir, "Python virtual environment") + .and_then(|_| { + inspect_real_file( + &layout.user.venv_dir.join("pyvenv.cfg"), + "Python virtual environment marker", + ) + }) + .and_then(|_| inspect_probe_executable(&python)); + if let Err(detail) = inspection { + return preflight_component( + "python-environment", + "Python Environment", + if required { "invalid" } else { "warning" }, + required, + None, + Some(&layout.user.venv_dir), + Some("PYTHON_ENVIRONMENT_INVALID"), + Some("Remove the damaged virtual environment and provision it again."), + Some(detail), + ); + } + + let output = match run_bounded_probe_command( + &python, + &["-I", "-E", "-s", "--version"], + PREFLIGHT_PROCESS_TIMEOUT, + ) { + Ok(output) => output, + Err(failure) => { + let (status, error_code, remediation) = python_probe_failure_mapping(failure.kind); + return preflight_component( + "python-environment", + "Python Environment", + status, + required, + None, + Some(&layout.user.venv_dir), + Some(error_code), + Some(remediation), + Some(failure.detail), + ); + } + }; + + match classify_python_version_output(&output) { + PythonVersionOutcome::Supported(version) => preflight_component( + "python-environment", + "Python Environment", + "ready", + required, + Some(version), + Some(&layout.user.venv_dir), + None, + None, + Some("Python 3.11 or 3.12 passed the bounded process probe.".to_string()), + ), + PythonVersionOutcome::Unsupported(version) => preflight_component( + "python-environment", + "Python Environment", + if required { "invalid" } else { "warning" }, + required, + Some(version), + Some(&layout.user.venv_dir), + Some("PYTHON_VERSION_UNSUPPORTED"), + Some("Provision a Python 3.11 or 3.12 desktop environment and retry."), + Some( + "The desktop Python version is outside the supported 3.11-3.12 range.".to_string(), + ), + ), + PythonVersionOutcome::Invalid => preflight_component( + "python-environment", + "Python Environment", + if required { "invalid" } else { "warning" }, + required, + None, + Some(&layout.user.venv_dir), + Some("PYTHON_ENVIRONMENT_INVALID"), + Some("Repair or provision the desktop Python environment and retry."), + Some("Python did not return the expected fixed version signature.".to_string()), + ), + } +} + +fn classify_settings_preflight_output(output: &ProbeCommandOutput) -> SettingsPreflightOutcome { + match (output.success, output.output.as_str()) { + (true, SETTINGS_PREFLIGHT_OK_TOKEN) => SettingsPreflightOutcome::Valid, + (false, SETTINGS_PREFLIGHT_INVALID_TOKEN) => SettingsPreflightOutcome::Invalid, + _ => SettingsPreflightOutcome::Error, + } +} + +fn probe_runtime_settings_component(layout: &RuntimeLayout) -> BootstrapPreflightComponent { + let required = layout.mode == RuntimeMode::Installed; + let path = &layout.user.config_file; + match fs::symlink_metadata(path) { + Err(error) if error.kind() == io::ErrorKind::NotFound => { + return preflight_component( + "runtime-settings", + "Runtime Settings", + "ready", + required, + None, + Some(path), + None, + None, + Some("No settings file exists; runtime defaults will be used.".to_string()), + ); + } + Err(error) => { + return preflight_component( + "runtime-settings", + "Runtime Settings", + "invalid", + required, + None, + Some(path), + Some("CONFIG_INVALID"), + Some("Correct or remove the damaged runtime settings file, then retry."), + Some(format!("cannot inspect runtime settings: {error}")), + ); + } + Ok(_) => {} + } + let inspection = (|| -> Result<(), String> { + validate_existing_path_chain(path, "runtime settings")?; + let metadata = fs::symlink_metadata(path) + .map_err(|error| format!("cannot inspect runtime settings: {error}"))?; + if metadata_is_link_or_reparse(&metadata) || !metadata.is_file() { + return Err("runtime settings must be a regular file".to_string()); + } + if metadata.len() > MAX_RUNTIME_SETTINGS_BYTES { + return Err(format!( + "runtime settings exceed the {} byte limit", + MAX_RUNTIME_SETTINGS_BYTES + )); + } + let mut bytes = Vec::with_capacity((metadata.len() as usize).saturating_add(1)); + fs::File::open(path) + .map_err(|error| format!("cannot open runtime settings: {error}"))? + .take(MAX_RUNTIME_SETTINGS_BYTES + 1) + .read_to_end(&mut bytes) + .map_err(|error| format!("cannot read runtime settings: {error}"))?; + if bytes.len() as u64 > MAX_RUNTIME_SETTINGS_BYTES { + return Err(format!( + "runtime settings changed beyond the {} byte limit", + MAX_RUNTIME_SETTINGS_BYTES + )); + } + let value: serde_json::Value = serde_json::from_slice(&bytes) + .map_err(|error| format!("runtime settings JSON is invalid: {error}"))?; + if !value.is_object() { + return Err("runtime settings JSON root must be an object".to_string()); + } + Ok(()) + })(); + if let Err(detail) = inspection { + return preflight_component( + "runtime-settings", + "Runtime Settings", + "invalid", + required, + None, + Some(path), + Some("CONFIG_INVALID"), + Some("Correct or remove the damaged runtime settings file, then retry."), + Some(detail), + ); + } + + if !required { + return preflight_component( + "runtime-settings", + "Runtime Settings", + "ready", + false, + None, + Some(path), + None, + None, + Some("The runtime settings JSON structure is valid.".to_string()), + ); + } + + let python = python_venv_executable(&layout.user.venv_dir); + let helper = layout + .backend_dir + .join("app") + .join("services") + .join("settings_preflight.py"); + if inspect_probe_executable(&python).is_err() + || inspect_real_file(&helper, "settings preflight helper").is_err() + { + return preflight_component( + "runtime-settings", + "Runtime Settings", + "blocked", + true, + None, + Some(path), + Some("CONFIG_PREFLIGHT_UNAVAILABLE"), + Some("Repair the desktop Python environment or application runtime, then retry."), + Some( + "RuntimeSettings semantic validation is unavailable in the installed runtime." + .to_string(), + ), + ); + } + + let helper_argument = helper.to_string_lossy().into_owned(); + let config_argument = path.to_string_lossy().into_owned(); + let arguments = ["-I", helper_argument.as_str(), config_argument.as_str()]; + let output = match run_bounded_probe_command(&python, &arguments, PREFLIGHT_PROCESS_TIMEOUT) { + Ok(output) => output, + Err(failure) => { + let (error_code, remediation, detail) = match failure.kind { + ProbeCommandFailureKind::Timeout => ( + "CONFIG_PREFLIGHT_TIMEOUT", + "Close stalled Python processes and retry.", + "RuntimeSettings semantic validation exceeded its time limit.", + ), + ProbeCommandFailureKind::PermissionDenied => ( + "CONFIG_PREFLIGHT_EXECUTION_BLOCKED", + "Restore permission to run the desktop Python environment and retry.", + "The desktop Python environment could not execute settings validation.", + ), + ProbeCommandFailureKind::Spawn | ProbeCommandFailureKind::Supervision => ( + "CONFIG_PREFLIGHT_ERROR", + "Repair the desktop Python environment or application runtime, then retry.", + "RuntimeSettings semantic validation could not run safely.", + ), + }; + return preflight_component( + "runtime-settings", + "Runtime Settings", + "blocked", + true, + None, + Some(path), + Some(error_code), + Some(remediation), + Some(detail.to_string()), + ); + } + }; + + match classify_settings_preflight_output(&output) { + SettingsPreflightOutcome::Valid => preflight_component( + "runtime-settings", + "Runtime Settings", + "ready", + true, + None, + Some(path), + None, + None, + Some( + "The settings file passed structural and RuntimeSettings semantic validation." + .to_string(), + ), + ), + SettingsPreflightOutcome::Invalid => preflight_component( + "runtime-settings", + "Runtime Settings", + "invalid", + true, + None, + Some(path), + Some("CONFIG_INVALID"), + Some("Correct or remove the damaged runtime settings file, then retry."), + Some("Runtime settings failed semantic validation.".to_string()), + ), + SettingsPreflightOutcome::Error => preflight_component( + "runtime-settings", + "Runtime Settings", + "blocked", + true, + None, + Some(path), + Some("CONFIG_PREFLIGHT_ERROR"), + Some("Repair the desktop Python environment or application runtime, then retry."), + Some( + "RuntimeSettings semantic validation returned an invalid fixed contract." + .to_string(), + ), + ), + } +} + +fn probe_port_component( + component_id: &str, + label: &str, + port: u16, + already_owned: bool, +) -> BootstrapPreflightComponent { + if already_owned { + return preflight_component( + component_id, + label, + "ready", + true, + None, + None, + None, + None, + Some(format!( + "localhost port {port} is owned by this desktop session." + )), + ); + } + match bind_desktop_proxy_at(port) { + Ok(listeners) => { + drop(listeners); + preflight_component( + component_id, + label, + "ready", + true, + None, + None, + None, + None, + Some(format!("localhost port {port} is available.")), + ) + } + Err(detail) => { + let (error_code, remediation) = if component_id == "desktop-proxy-port" { + ( + "PORT_IN_USE", + "Close the process using localhost port 18000 and retry.", + ) + } else { + ( + "PRIVATE_PORT_IN_USE", + "Close the process using the private backend port and retry.", + ) + }; + preflight_component( + component_id, + label, + "blocked", + true, + None, + None, + Some(error_code), + Some(remediation), + Some(detail), + ) + } + } +} + +fn build_bootstrap_preflight( + app: &AppHandle, + layout: &RuntimeLayout, + proxy_started: bool, +) -> BootstrapPreflightReport { + let webview = if app.get_webview_window("main").is_some() { + preflight_component( + "webview2", + "Microsoft Edge WebView2", + "ready", + true, + None, + None, + None, + None, + Some("The desktop WebView is available.".to_string()), + ) + } else { + preflight_component( + "webview2", + "Microsoft Edge WebView2", + "blocked", + true, + None, + None, + Some("WEBVIEW2_MISSING"), + Some( + "Install or repair Microsoft Edge WebView2 Runtime, then restart the application.", + ), + Some("The main desktop WebView is unavailable.".to_string()), + ) + }; + finalize_preflight_report(vec![ + probe_desktop_runtime_component(layout), + probe_data_root_component(layout), + probe_uv_component(layout), + probe_python_environment_component(layout), + probe_media_tool_component(layout, "ffmpeg", "FFmpeg"), + probe_media_tool_component(layout, "ffprobe", "FFprobe"), + probe_port_component( + "desktop-proxy-port", + "Desktop Proxy Port", + BACKEND_PORT, + proxy_started, + ), + probe_port_component( + "backend-private-port", + "Backend Private Port", + layout.backend_port, + false, + ), + probe_runtime_settings_component(layout), + webview, + ]) +} + +fn preflight_report_from_failure(failure: &BootstrapFailure) -> BootstrapPreflightReport { + let mut report = finalize_preflight_report( + PREFLIGHT_COMPONENT_ORDER + .iter() + .map(|component_id| { + preflight_component( + component_id, + preflight_component_label(component_id), + "blocked", + true, + None, + None, + Some("PREFLIGHT_SCAN_BLOCKED"), + Some( + "Resolve the reported bootstrap failure, then retry the environment scan.", + ), + Some(format!( + "{} could not be evaluated because bootstrap initialization stopped early.", + preflight_component_label(component_id) + )), + ) + }) + .collect(), + ); + apply_failure_to_preflight(&mut report, failure); + report +} + +fn bootstrap_internal_failure(error: impl Into) -> BootstrapFailure { + BootstrapFailure::manual( + "DESKTOP_BOOTSTRAP_FAILED", + "desktop-bootstrap", + "Open diagnostics, restart the application, and retry.", + error, + None, + ) +} + +fn bootstrap_wait_failure(error: BackendWaitFailure, layout: &RuntimeLayout) -> BootstrapFailure { + let local_path = Some(layout.backend_dir.as_path()); + match error { + BackendWaitFailure::ProcessExited(detail) => BootstrapFailure::retryable( + "BACKEND_EXITED", + "python-runtime", + "Open the logs, correct the reported runtime or configuration error, and retry.", + detail, + local_path, + ), + BackendWaitFailure::Incompatible(detail) => BootstrapFailure::manual( + "BACKEND_HEALTH_INVALID", + "backend-health", + "Repair or reinstall the matching desktop runtime, then retry.", + detail, + local_path, + ), + BackendWaitFailure::Occupied(detail) => BootstrapFailure::retryable( + "PRIVATE_PORT_IN_USE", + "backend-port", + "Close the conflicting local process and retry.", + detail, + None, + ), + BackendWaitFailure::Timeout => BootstrapFailure::retryable( + "BACKEND_START_TIMEOUT", + "backend-health", + "Open the logs, check runtime initialization and network access, then retry.", + BackendWaitFailure::Timeout.to_string(), + local_path, + ), + BackendWaitFailure::Internal(detail) => bootstrap_internal_failure(detail), + } +} + +fn prepare_bootstrap_runtime(app: &AppHandle) -> Result { + let controller = app.state::(); + if let Some(layout) = controller + .runtime + .lock() + .map_err(|_| bootstrap_internal_failure("bootstrap runtime lock poisoned"))? + .layout + .clone() + { + return Ok(layout); + } + + let layout = resolve_runtime_layout(app)?; + let mut runtime = controller + .runtime + .lock() + .map_err(|_| bootstrap_internal_failure("bootstrap runtime lock poisoned"))?; + if runtime.layout.is_none() { + runtime.layout = Some(layout.clone()); + } + Ok(runtime.layout.clone().unwrap_or(layout)) +} + +fn ensure_bootstrap_proxy( + app: &AppHandle, + layout: &RuntimeLayout, + attempt_epoch: u64, +) -> Result { + let controller = app.state::(); + let mut runtime = controller + .runtime + .lock() + .map_err(|_| bootstrap_internal_failure("bootstrap runtime lock poisoned"))?; + if !bootstrap_attempt_is_current(&runtime, attempt_epoch) { + return Ok(false); + } + if runtime.proxy_started { + return Ok(true); } let listeners = bind_desktop_proxy_at(BACKEND_PORT).map_err(|error| { @@ -4321,6 +5931,35 @@ fn bootstrap_attempt_is_current_for_app( Ok(bootstrap_attempt_is_current(&runtime, attempt_epoch)) } +fn bootstrap_proxy_state_for_attempt( + app: &AppHandle, + attempt_epoch: u64, +) -> Result, BootstrapFailure> { + let controller = app.state::(); + let runtime = controller + .runtime + .lock() + .map_err(|_| bootstrap_internal_failure("bootstrap runtime lock poisoned"))?; + Ok(bootstrap_attempt_is_current(&runtime, attempt_epoch).then_some(runtime.proxy_started)) +} + +fn cache_preflight_for_attempt( + app: &AppHandle, + attempt_epoch: u64, + report: BootstrapPreflightReport, +) -> Result { + let controller = app.state::(); + let mut runtime = controller + .runtime + .lock() + .map_err(|_| bootstrap_internal_failure("bootstrap runtime lock poisoned"))?; + if !bootstrap_attempt_is_current(&runtime, attempt_epoch) { + return Ok(false); + } + runtime.preflight = report; + Ok(true) +} + fn set_bootstrap_phase_for_attempt( app: &AppHandle, attempt_epoch: u64, @@ -4346,13 +5985,15 @@ fn publish_bootstrap_failure_for_attempt( failure: &BootstrapFailure, ) -> Result { let controller = app.state::(); - let runtime = controller + let mut runtime = controller .runtime .lock() .map_err(|_| "bootstrap runtime lock poisoned".to_string())?; if !bootstrap_attempt_is_current(&runtime, attempt_epoch) { return Ok(false); } + apply_failure_to_preflight(&mut runtime.preflight, failure); + drop(runtime); set_bootstrap_failure(app, failure)?; Ok(true) } @@ -4364,7 +6005,24 @@ fn run_bootstrap_attempt( if !bootstrap_attempt_is_current_for_app(app, attempt_epoch)? { return Ok(BootstrapAttemptOutcome::Cancelled); } - let layout = prepare_bootstrap_runtime(app)?; + let layout = match prepare_bootstrap_runtime(app) { + Ok(layout) => layout, + Err(failure) => { + let report = preflight_report_from_failure(&failure); + let _ = cache_preflight_for_attempt(app, attempt_epoch, report); + return Err(failure); + } + }; + let Some(proxy_started) = bootstrap_proxy_state_for_attempt(app, attempt_epoch)? else { + return Ok(BootstrapAttemptOutcome::Cancelled); + }; + let preflight = build_bootstrap_preflight(app, &layout, proxy_started); + if !cache_preflight_for_attempt(app, attempt_epoch, preflight.clone())? { + return Ok(BootstrapAttemptOutcome::Cancelled); + } + if let Some(failure) = preflight_blocking_failure(&preflight) { + return Err(failure); + } if !ensure_bootstrap_proxy(app, &layout, attempt_epoch)? { return Ok(BootstrapAttemptOutcome::Cancelled); } @@ -4461,6 +6119,7 @@ fn begin_bootstrap_attempt(runtime: &mut BootstrapRuntime) -> Option { } runtime.attempt_epoch = next_bootstrap_epoch(runtime.attempt_epoch); runtime.attempt_running = true; + runtime.preflight = BootstrapPreflightReport::default(); Some(runtime.attempt_epoch) } @@ -4877,6 +6536,7 @@ fn main() { .plugin(tauri_plugin_dialog::init()) .invoke_handler(tauri::generate_handler![ backend_get_status, + bootstrap_get_preflight, backend_get_logs, backend_start, backend_stop, @@ -5840,6 +7500,515 @@ mod tests { assert_eq!(next_lifecycle_generation(u64::MAX), 1); } + #[test] + fn preflight_report_is_ordered_and_uses_stable_overall_precedence() { + let report = finalize_preflight_report(vec![ + preflight_component( + "webview2", "WebView2", "ready", true, None, None, None, None, None, + ), + preflight_component( + "runtime-settings", + "Runtime Settings", + "invalid", + true, + None, + None, + Some("CONFIG_INVALID"), + Some("Correct the settings file."), + Some("invalid JSON".to_string()), + ), + preflight_component( + "desktop-runtime", + "Desktop Runtime", + "ready", + true, + Some("0.4.1".to_string()), + None, + None, + None, + None, + ), + ]); + + assert_eq!(report.schema_version, 1); + assert_eq!(report.overall_status, "needs_repair"); + assert_eq!( + report + .components + .iter() + .map(|component| component.component_id.as_str()) + .collect::>(), + ["desktop-runtime", "runtime-settings", "webview2"] + ); + let serialized = serde_json::to_value(&report).expect("preflight should serialize"); + assert!(serialized["components"][0]["path"].is_null()); + assert!(serialized["components"][0]["error_code"].is_null()); + + let blocked = finalize_preflight_report(vec![preflight_component( + "backend-private-port", + "Backend Private Port", + "blocked", + true, + None, + None, + Some("PRIVATE_PORT_IN_USE"), + None, + None, + )]); + assert_eq!(blocked.overall_status, "blocked"); + } + + #[test] + fn default_preflight_matches_the_shared_scanning_contract() { + let report = BootstrapPreflightReport::default(); + assert_eq!(report.overall_status, "scanning"); + assert_eq!( + report + .components + .iter() + .map(|component| component.component_id.as_str()) + .collect::>(), + PREFLIGHT_COMPONENT_ORDER + ); + assert!(report + .components + .iter() + .all(|component| component.status == "scanning")); + + let expected: serde_json::Value = serde_json::from_str(include_str!( + "../../src/lib/fixtures/bootstrap-preflight-scanning-v1.json" + )) + .expect("shared scanning fixture should be valid JSON"); + let actual = serde_json::to_value(report).expect("default preflight should serialize"); + assert_eq!(actual, expected); + } + + #[test] + fn preflight_fields_are_utf8_safe_and_bounded() { + let long_detail = "界".repeat(MAX_PREFLIGHT_DETAIL_BYTES); + let long_path = PathBuf::from(format!("C:/{}", "路".repeat(MAX_PREFLIGHT_PATH_BYTES))); + let component = preflight_component( + "runtime-settings", + "Runtime Settings", + "invalid", + true, + Some("v".repeat(512)), + Some(&long_path), + Some("CONFIG_INVALID"), + Some(&long_detail), + Some(long_detail.clone()), + ); + + assert!(component.version.expect("version should exist").len() <= 128); + assert!(component.path.expect("path should exist").len() <= MAX_PREFLIGHT_PATH_BYTES); + assert!( + component + .remediation + .expect("remediation should exist") + .len() + <= MAX_PREFLIGHT_DETAIL_BYTES + ); + assert!(component.detail.expect("detail should exist").len() <= MAX_PREFLIGHT_DETAIL_BYTES); + + let (retained, truncated) = + read_bounded_probe_stream(io::Cursor::new(vec![b'x'; MAX_PREFLIGHT_OUTPUT_BYTES * 4])) + .expect("bounded probe output should be readable"); + assert_eq!(retained.len(), MAX_PREFLIGHT_OUTPUT_BYTES); + assert!(truncated); + } + + #[test] + fn invalid_settings_have_a_stable_config_error_without_mutation() { + let temporary = TestDirectory::new("preflight-invalid-settings"); + let user = UserPaths::new(temporary.path.join("user")); + fs::create_dir_all(&user.config_dir).expect("config directory should be created"); + write_file(&user.config_file, "{ invalid"); + let mut layout = proxy_test_layout(43122); + layout.mode = RuntimeMode::Installed; + layout.user = user; + + let before = fs::read(&layout.user.config_file).expect("fixture should be readable"); + let component = probe_runtime_settings_component(&layout); + let after = fs::read(&layout.user.config_file).expect("fixture should remain readable"); + + assert_eq!(component.status, "invalid"); + assert!(component.required); + assert_eq!(component.error_code.as_deref(), Some("CONFIG_INVALID")); + assert_eq!(before, after); + } + + #[test] + fn python_environment_probe_is_read_only_and_missing_is_stable() { + let temporary = TestDirectory::new("preflight-python-environment"); + let mut layout = proxy_test_layout(43122); + layout.mode = RuntimeMode::Installed; + layout.user = UserPaths::new(temporary.path.join("user")); + + assert!(!layout.user.venv_dir.exists()); + let component = probe_python_environment_component(&layout); + assert!(!layout.user.venv_dir.exists()); + assert_eq!(component.status, "missing"); + assert_eq!( + component.error_code.as_deref(), + Some("PYTHON_ENVIRONMENT_MISSING") + ); + } + + #[test] + fn preflight_error_classification_maps_required_failures_to_bootstrap() { + let report = finalize_preflight_report(vec![preflight_component( + "ffmpeg", + "FFmpeg", + "missing", + true, + None, + None, + Some("FFMPEG_MISSING"), + Some("Install FFmpeg."), + Some("ffmpeg.exe was not found".to_string()), + )]); + let failure = + preflight_blocking_failure(&report).expect("required missing tool should block"); + assert_eq!(failure.error_code, "FFMPEG_MISSING"); + assert_eq!(failure.component_id, "ffmpeg"); + assert!(!failure.retryable); + + assert_eq!( + tool_probe_failure_mapping("ffprobe", ProbeCommandFailureKind::Timeout).1, + "FFPROBE_PROBE_TIMEOUT" + ); + assert_eq!( + tool_probe_failure_mapping("ffmpeg", ProbeCommandFailureKind::PermissionDenied).1, + "FFMPEG_EXECUTION_BLOCKED" + ); + + let uv_failure = runtime_resolution_failure( + "manifest-declared runtime files are missing: bin/uv.exe".to_string(), + Some(Path::new("C:/Program Files/MPP/runtime")), + ); + assert_eq!(uv_failure.error_code, "UV_MISSING"); + assert_eq!(uv_failure.component_id, "bundled-uv"); + assert_eq!( + runtime_resolution_failure( + "runtime root is incomplete: missing uv.lock".to_string(), + None, + ) + .error_code, + "RUNTIME_INVALID" + ); + + let early_report = preflight_report_from_failure(&uv_failure); + assert_eq!( + early_report.components.len(), + PREFLIGHT_COMPONENT_ORDER.len() + ); + assert_eq!( + early_report + .components + .iter() + .map(|component| component.component_id.as_str()) + .collect::>(), + PREFLIGHT_COMPONENT_ORDER + ); + assert_eq!( + early_report + .components + .iter() + .find(|component| component.component_id == "bundled-uv") + .and_then(|component| component.error_code.as_deref()), + Some("UV_MISSING") + ); + + let mut raced_port_report = finalize_preflight_report(vec![preflight_component( + "desktop-proxy-port", + "Desktop Proxy Port", + "ready", + true, + None, + None, + None, + None, + None, + )]); + let raced_port_failure = BootstrapFailure::retryable( + "PORT_IN_USE", + "desktop-proxy", + "Close the conflicting process and retry.", + "localhost port 18000 was claimed after preflight", + None, + ); + apply_failure_to_preflight(&mut raced_port_report, &raced_port_failure); + assert_eq!(raced_port_report.overall_status, "blocked"); + assert_eq!( + raced_port_report.components[0].error_code.as_deref(), + Some("PORT_IN_USE") + ); + } + + #[test] + fn non_contract_failure_components_stay_within_the_fixed_preflight_set() { + let cases = [ + ( + BootstrapFailure::manual( + "BACKEND_HEALTH_INVALID", + "backend-health", + "Repair the runtime.", + "health contract mismatch", + None, + ), + "python-environment", + ), + ( + bootstrap_internal_failure("bootstrap runtime lock poisoned"), + "desktop-runtime", + ), + ( + BootstrapFailure::retryable( + "DESKTOP_SESSION_INIT_FAILED", + "desktop-session", + "Restart the application.", + "session entropy source unavailable", + None, + ), + "desktop-runtime", + ), + ]; + + for (failure, expected_component_id) in cases { + let report = preflight_report_from_failure(&failure); + assert_eq!(report.components.len(), PREFLIGHT_COMPONENT_ORDER.len()); + assert_eq!( + report + .components + .iter() + .map(|component| component.component_id.as_str()) + .collect::>(), + PREFLIGHT_COMPONENT_ORDER + ); + assert_eq!( + report + .components + .iter() + .find(|component| component.component_id == expected_component_id) + .and_then(|component| component.error_code.as_deref()), + Some(failure.error_code.as_str()) + ); + } + } + + #[test] + fn python_version_probe_accepts_only_supported_fixed_signatures() { + for version in ["3.11.0", "3.11.10", "3.12.9"] { + assert_eq!( + classify_python_version_output(&ProbeCommandOutput { + success: true, + output: format!("Python {version}"), + }), + PythonVersionOutcome::Supported(version.to_string()) + ); + } + for version in ["2.7.18", "3.10.14", "3.13.0"] { + assert_eq!( + classify_python_version_output(&ProbeCommandOutput { + success: true, + output: format!("Python {version}"), + }), + PythonVersionOutcome::Unsupported(version.to_string()) + ); + } + for output in [ + ProbeCommandOutput { + success: false, + output: "Python 3.11.10".to_string(), + }, + ProbeCommandOutput { + success: true, + output: "Python 3.11".to_string(), + }, + ProbeCommandOutput { + success: true, + output: "Python 3.11.0rc1".to_string(), + }, + ProbeCommandOutput { + success: true, + output: "python 3.11.10".to_string(), + }, + ProbeCommandOutput { + success: true, + output: "Python 3.11.10 extra".to_string(), + }, + ProbeCommandOutput { + success: true, + output: "arbitrary executable output".to_string(), + }, + ] { + assert_eq!( + classify_python_version_output(&output), + PythonVersionOutcome::Invalid + ); + } + + assert_eq!( + python_probe_failure_mapping(ProbeCommandFailureKind::Timeout).1, + "PYTHON_PROBE_TIMEOUT" + ); + assert_eq!( + python_probe_failure_mapping(ProbeCommandFailureKind::PermissionDenied).1, + "PYTHON_EXECUTION_BLOCKED" + ); + assert_eq!( + python_probe_failure_mapping(ProbeCommandFailureKind::Supervision).1, + "PYTHON_PROBE_BLOCKED" + ); + assert_eq!( + python_probe_failure_mapping(ProbeCommandFailureKind::Spawn).1, + "PYTHON_ENVIRONMENT_INVALID" + ); + } + + #[test] + fn media_tool_probe_requires_the_matching_fixed_version_signature() { + let ffmpeg = ProbeCommandOutput { + success: true, + output: "ffmpeg version 7.1.1-full_build Copyright FFmpeg".to_string(), + }; + let ffprobe = ProbeCommandOutput { + success: true, + output: "ffprobe version 7.1.1-full_build Copyright FFmpeg".to_string(), + }; + assert_eq!( + validated_media_tool_version_line("ffmpeg", &ffmpeg).as_deref(), + Some("ffmpeg version 7.1.1-full_build Copyright FFmpeg") + ); + assert_eq!( + validated_media_tool_version_line("ffprobe", &ffprobe).as_deref(), + Some("ffprobe version 7.1.1-full_build Copyright FFmpeg") + ); + + for (tool, output) in [ + ("ffmpeg", "ffprobe version 7.1.1"), + ("ffprobe", "ffmpeg version 7.1.1"), + ("ffmpeg", "arbitrary executable output"), + ("ffprobe", "ffprobe version "), + ] { + assert!(validated_media_tool_version_line( + tool, + &ProbeCommandOutput { + success: true, + output: output.to_string(), + } + ) + .is_none()); + } + assert!(validated_media_tool_version_line( + "ffmpeg", + &ProbeCommandOutput { + success: false, + output: "ffmpeg version 7.1.1".to_string(), + } + ) + .is_none()); + } + + #[test] + fn settings_preflight_accepts_only_the_fixed_credential_safe_contract() { + assert_eq!( + classify_settings_preflight_output(&ProbeCommandOutput { + success: true, + output: SETTINGS_PREFLIGHT_OK_TOKEN.to_string(), + }), + SettingsPreflightOutcome::Valid + ); + assert_eq!( + classify_settings_preflight_output(&ProbeCommandOutput { + success: false, + output: SETTINGS_PREFLIGHT_INVALID_TOKEN.to_string(), + }), + SettingsPreflightOutcome::Invalid + ); + for output in [ + ProbeCommandOutput { + success: true, + output: SETTINGS_PREFLIGHT_INVALID_TOKEN.to_string(), + }, + ProbeCommandOutput { + success: false, + output: SETTINGS_PREFLIGHT_OK_TOKEN.to_string(), + }, + ProbeCommandOutput { + success: false, + output: "secret-bearing validation error".to_string(), + }, + ] { + assert_eq!( + classify_settings_preflight_output(&output), + SettingsPreflightOutcome::Error + ); + } + } + + #[test] + fn data_root_preflight_rechecks_writability_and_cleans_probes() { + let temporary = TestDirectory::new("preflight-data-root"); + let mut layout = proxy_test_layout(43122); + layout.mode = RuntimeMode::Installed; + layout.user = UserPaths::new(temporary.path.join("user")); + for directory in + std::iter::once(layout.user.root.as_path()).chain(layout.user.required_directories()) + { + fs::create_dir_all(directory).expect("data directory should be created"); + } + + let component = probe_data_root_component(&layout); + assert_eq!(component.status, "ready"); + for directory in + std::iter::once(layout.user.root.as_path()).chain(layout.user.required_directories()) + { + assert!( + fs::read_dir(directory) + .expect("data directory should be readable") + .all(|entry| !entry + .expect("directory entry should be readable") + .file_name() + .to_string_lossy() + .starts_with(".preflight-write-")), + "write probes must be removed" + ); + } + } + + #[cfg(windows)] + #[test] + fn preflight_process_probe_has_a_hard_timeout_and_reclaims_the_tree() { + let system_root = env::var_os("SystemRoot").expect("Windows test requires SystemRoot"); + let command = PathBuf::from(system_root).join("System32").join("cmd.exe"); + let started = Instant::now(); + let failure = run_bounded_probe_command( + &command, + &["/D", "/C", "ping -n 30 127.0.0.1 >NUL"], + Duration::from_millis(150), + ) + .expect_err("long-running probe should time out"); + + assert_eq!(failure.kind, ProbeCommandFailureKind::Timeout); + assert!(started.elapsed() < Duration::from_secs(5)); + } + + #[cfg(unix)] + #[test] + fn preflight_process_probe_has_a_hard_timeout_and_reclaims_the_tree() { + let started = Instant::now(); + let failure = run_bounded_probe_command( + Path::new("/bin/sh"), + &["-c", "sleep 30 & wait"], + Duration::from_millis(150), + ) + .expect_err("long-running probe should time out"); + + assert_eq!(failure.kind, ProbeCommandFailureKind::Timeout); + assert!(started.elapsed() < Duration::from_secs(5)); + } + #[cfg(windows)] #[test] fn backend_process_enters_its_job_before_executing() { diff --git a/web/src/components/pages/bootstrap-page.test.tsx b/web/src/components/pages/bootstrap-page.test.tsx index f39eafd..44da1a6 100644 --- a/web/src/components/pages/bootstrap-page.test.tsx +++ b/web/src/components/pages/bootstrap-page.test.tsx @@ -10,6 +10,9 @@ import { type BootstrapBridge, type BootstrapDiagnostics, type BootstrapPhase, + type BootstrapPreflight, + type BootstrapPreflightComponent, + type BootstrapPreflightOverallStatus, type BootstrapStatus, } from "@/lib/tauri" @@ -51,7 +54,38 @@ function bootstrapStatus( } } -function createBridge(initial: BootstrapStatus) { +function preflightComponent( + overrides: Partial = {}, +): BootstrapPreflightComponent { + return { + component_id: "desktop-runtime", + label: "桌面运行时", + status: "ready", + required: true, + version: "1.0.0", + path: "C:\\Program Files\\MediaProcessPipeline\\runtime", + error_code: null, + remediation: null, + detail: null, + ...overrides, + } +} + +function preflightReport( + overallStatus: BootstrapPreflightOverallStatus = "ready", + components: BootstrapPreflightComponent[] = [preflightComponent()], +): BootstrapPreflight { + return { + schema_version: 1, + overall_status: overallStatus, + components, + } +} + +function createBridge( + initial: BootstrapStatus, + preflight: BootstrapPreflight = preflightReport(), +) { let statusListener: ((status: BootstrapStatus) => void) | undefined const diagnostics: BootstrapDiagnostics = { status: initial, @@ -66,6 +100,7 @@ function createBridge(initial: BootstrapStatus) { const unsubscribe = vi.fn() const bridge: BootstrapBridge = { getStatus: vi.fn().mockResolvedValue(initial), + getPreflight: vi.fn().mockResolvedValue(preflight), retry: vi.fn().mockResolvedValue(bootstrapStatus("STARTING_BACKEND")), openLogs: vi.fn().mockResolvedValue(undefined), getDiagnostics: vi.fn().mockResolvedValue(diagnostics), @@ -129,6 +164,88 @@ describe("BootstrapPage", () => { expect(fetchMock).not.toHaveBeenCalled() }) + it("summarizes a ready preflight and its required components", async () => { + const harness = createBridge( + bootstrapStatus("READY_TO_START"), + preflightReport("ready", [ + preflightComponent(), + preflightComponent({ + component_id: "ffmpeg", + label: "FFmpeg", + version: "7.1", + }), + ]), + ) + getBootstrapBridgeMock.mockReturnValue(harness.bridge) + window.history.replaceState({}, "", "/?bootstrap=1") + + render() + + expect(await screen.findByText("运行环境检查通过", { exact: false })).toBeInTheDocument() + expect(screen.getByText("已通过")).toBeInTheDocument() + expect(screen.getByText("2/2 项必需组件就绪", { exact: false })).toBeInTheDocument() + expect(screen.getByText("FFmpeg")).toBeInTheDocument() + expect(screen.getAllByText("已就绪")).toHaveLength(2) + }) + + it("shows a missing component and its detailed repair fields", async () => { + const harness = createBridge( + bootstrapStatus("FAILED_MANUAL"), + preflightReport("needs_repair", [ + preflightComponent({ + status: "missing", + version: null, + path: null, + error_code: "RUNTIME_MISSING", + remediation: "重新安装桌面运行时。", + detail: "runtime-manifest.json 未找到。", + }), + ]), + ) + getBootstrapBridgeMock.mockReturnValue(harness.bridge) + window.history.replaceState({}, "", "/?bootstrap=1") + + render() + + expect(await screen.findByText("需要修复本地组件", { exact: false })).toBeInTheDocument() + expect(screen.getByText("组件缺失")).toBeInTheDocument() + fireEvent.click(screen.getByRole("button", { name: "展开诊断" })) + + expect(await screen.findByText("RUNTIME_MISSING")).toBeInTheDocument() + expect(screen.getByText("重新安装桌面运行时。")).toBeInTheDocument() + expect(screen.getByText("runtime-manifest.json 未找到。")).toBeInTheDocument() + }) + + it("shows configuration-invalid preflight results", async () => { + const harness = createBridge( + bootstrapStatus("FAILED_MANUAL"), + preflightReport("needs_configuration", [ + preflightComponent({ + component_id: "data-root", + label: "数据目录", + status: "invalid", + version: null, + path: "D:\\MPP Data", + error_code: "DATA_ROOT_INVALID", + remediation: "选择可写的数据目录。", + detail: "当前目录不可写。", + }), + ]), + ) + getBootstrapBridgeMock.mockReturnValue(harness.bridge) + window.history.replaceState({}, "", "/?bootstrap=1") + + render() + + expect(await screen.findByText("需要完成运行配置", { exact: false })).toBeInTheDocument() + expect(screen.getByText("待配置")).toBeInTheDocument() + expect(screen.getByText("配置无效")).toBeInTheDocument() + fireEvent.click(screen.getByRole("button", { name: "展开诊断" })) + + expect(await screen.findByText("DATA_ROOT_INVALID")).toBeInTheDocument() + expect(screen.getByText("D:\\MPP Data")).toBeInTheDocument() + }) + it("shows stable failure fields and supports retry, logs, and diagnostics", async () => { const failed = bootstrapStatus("FAILED_RETRYABLE", { message: "桌面代理端口已被其他进程占用。", @@ -166,6 +283,8 @@ describe("BootstrapPage", () => { }) it("provides a manual recovery state when the desktop bridge is unavailable", async () => { + const fetchMock = vi.fn() + vi.stubGlobal("fetch", fetchMock) getBootstrapBridgeMock.mockReturnValue(undefined) window.history.replaceState({}, "", "/?bootstrap=1") @@ -173,7 +292,9 @@ describe("BootstrapPage", () => { expect(await screen.findByText("DESKTOP_BRIDGE_UNAVAILABLE")).toBeInTheDocument() expect(screen.getByText("desktop-shell")).toBeInTheDocument() + expect(screen.getByText("桌面环境检查仅在应用内提供。")).toBeInTheDocument() expect(screen.queryByRole("button", { name: "重试" })).not.toBeInTheDocument() + expect(fetchMock).not.toHaveBeenCalled() fireEvent.click(screen.getByRole("button", { name: "打开日志目录" })) expect(screen.getByText("桌面日志接口当前不可用,请通过桌面应用入口重新打开。")).toBeInTheDocument() diff --git a/web/src/components/pages/bootstrap-page.tsx b/web/src/components/pages/bootstrap-page.tsx index d1b1aa5..5bf3e1d 100644 --- a/web/src/components/pages/bootstrap-page.tsx +++ b/web/src/components/pages/bootstrap-page.tsx @@ -18,6 +18,9 @@ import { type BootstrapBridge, type BootstrapDiagnostics, type BootstrapPhase, + type BootstrapPreflight, + type BootstrapPreflightComponentStatus, + type BootstrapPreflightOverallStatus, type BootstrapStatus, } from "@/lib/tauri" import { cn } from "@/lib/utils" @@ -76,6 +79,40 @@ const phaseCopy: Record = { + scanning: "正在检查运行环境", + ready: "运行环境检查通过", + needs_configuration: "需要完成运行配置", + needs_repair: "需要修复本地组件", + blocked: "运行环境阻止启动", +} + +const preflightBadgeCopy: Record = { + scanning: "检查中", + ready: "已通过", + needs_configuration: "待配置", + needs_repair: "待修复", + blocked: "已阻止", +} + +const componentStatusCopy: Record = { + scanning: "检查中", + ready: "已就绪", + warning: "需要留意", + missing: "组件缺失", + invalid: "配置无效", + blocked: "已阻止", +} + +const componentStatusClassName: Record = { + scanning: "border-slate-200 bg-slate-50 text-slate-600 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300", + ready: "border-sky-200 bg-sky-50 text-sky-700 dark:border-sky-900 dark:bg-sky-950/50 dark:text-sky-300", + warning: "border-blue-200 bg-blue-50 text-blue-700 dark:border-blue-900 dark:bg-blue-950/50 dark:text-blue-300", + missing: "border-red-200 bg-red-50 text-red-700 dark:border-red-900 dark:bg-red-950/50 dark:text-red-300", + invalid: "border-red-200 bg-red-50 text-red-700 dark:border-red-900 dark:bg-red-950/50 dark:text-red-300", + blocked: "border-red-200 bg-red-50 text-red-700 dark:border-red-900 dark:bg-red-950/50 dark:text-red-300", +} + function failureStatus( phase: "FAILED_RETRYABLE" | "FAILED_MANUAL", errorCode: string, @@ -125,6 +162,9 @@ export function BootstrapPage() { const [diagnostics, setDiagnostics] = useState(null) const [diagnosticsLoading, setDiagnosticsLoading] = useState(false) const [diagnosticsError, setDiagnosticsError] = useState(null) + const [preflight, setPreflight] = useState(null) + const [preflightLoading, setPreflightLoading] = useState(true) + const [preflightError, setPreflightError] = useState(null) const copy = phaseCopy[status.phase] const isFailure = status.phase === "FAILED_RETRYABLE" || status.phase === "FAILED_MANUAL" @@ -135,6 +175,8 @@ export function BootstrapPage() { bridgeRef.current = bridge if (!bridge) { + setPreflightLoading(false) + setPreflightError("桌面环境检查仅在应用内提供。") setStatus(failureStatus( "FAILED_MANUAL", "DESKTOP_BRIDGE_UNAVAILABLE", @@ -145,9 +187,28 @@ export function BootstrapPage() { } let active = true + let lastPreflightPhase: BootstrapPhase = "SCANNING" + + const loadPreflight = async (showLoading: boolean) => { + if (showLoading && active) setPreflightLoading(true) + try { + const nextPreflight = await bridge.getPreflight() + if (!active) return + setPreflight(nextPreflight) + setPreflightError(null) + } catch { + if (active) setPreflightError("环境检查数据暂时不可用,请稍后重试。") + } finally { + if (active) setPreflightLoading(false) + } + } + + void loadPreflight(true) void bridge.getStatus() .then((nextStatus) => { - if (active) setStatus(nextStatus) + if (!active) return + lastPreflightPhase = nextStatus.phase + setStatus(nextStatus) }) .catch(() => { if (!active) return @@ -160,7 +221,12 @@ export function BootstrapPage() { }) const unsubscribe = bridge.onStatus((nextStatus) => { - if (active) setStatus(nextStatus) + if (!active) return + setStatus(nextStatus) + if (nextStatus.phase !== lastPreflightPhase) { + lastPreflightPhase = nextStatus.phase + void loadPreflight(false) + } }) return () => { @@ -177,6 +243,8 @@ export function BootstrapPage() { setActionError(null) setDiagnostics(null) setDiagnosticsError(null) + setPreflightLoading(true) + setPreflightError(null) setStatus((current) => ({ ...current, state: "starting", @@ -189,6 +257,11 @@ export function BootstrapPage() { })) try { setStatus(await bridge.retry()) + try { + setPreflight(await bridge.getPreflight()) + } catch { + setPreflightError("环境检查数据暂时不可用,请稍后重试。") + } } catch { setStatus(failureStatus( "FAILED_RETRYABLE", @@ -198,6 +271,7 @@ export function BootstrapPage() { )) } finally { setBusy(false) + setPreflightLoading(false) } } @@ -240,6 +314,8 @@ export function BootstrapPage() { const errorCode = status.error_code ?? "BOOTSTRAP_FAILED" const componentId = status.component_id ?? "desktop-bootstrap" const remediation = status.remediation ?? "请打开日志目录确认诊断信息,然后重新启动应用。" + const requiredComponents = preflight?.components.filter((component) => component.required) ?? [] + const readyRequiredComponents = requiredComponents.filter((component) => component.status === "ready") return (
@@ -337,6 +413,71 @@ export function BootstrapPage() { })} +
+
+
+

环境检查

+ {preflight ? ( +

+ {preflightStatusCopy[preflight.overall_status]} · {readyRequiredComponents.length}/{requiredComponents.length} 项必需组件就绪 +

+ ) : ( +

+ 正在读取本地组件状态 +

+ )} +
+ {preflight && ( + + {preflightBadgeCopy[preflight.overall_status]} + + )} +
+ + {preflightLoading && !preflight && ( +
+ + 正在检查本地环境 +
+ )} + {preflightError && ( +

+ {preflightError} +

+ )} + {preflight && ( +
    + {preflight.components.map((component) => ( +
  • +
    +
    +

    + {component.label} +

    +

    + {component.required ? "必需组件" : "可选组件"} + {component.version ? ` · ${component.version}` : ""} +

    +
    + + {componentStatusCopy[component.status]} + +
    +
  • + ))} +
+ )} +
+ {isFailure && (

错误详情

@@ -433,6 +574,38 @@ export function BootstrapPage() { {diagnosticsError}

)} + {preflight && ( +
+

+ 组件详情 +

+ {preflight.components.map((component) => ( +
+
component_id
+
{component.component_id}
+
label
+
{component.label}
+
status
+
{component.status}
+
required
+
{String(component.required)}
+
version
+
{component.version ?? "—"}
+
path
+
{component.path ?? "—"}
+
error_code
+
{component.error_code ?? "—"}
+
remediation
+
{component.remediation ?? "—"}
+
detail
+
{component.detail ?? "—"}
+
+ ))} +
+ )} {diagnostics && !diagnosticsLoading && (
diff --git a/web/src/lib/fixtures/bootstrap-preflight-scanning-v1.json b/web/src/lib/fixtures/bootstrap-preflight-scanning-v1.json new file mode 100644 index 0000000..96d0c7d --- /dev/null +++ b/web/src/lib/fixtures/bootstrap-preflight-scanning-v1.json @@ -0,0 +1,116 @@ +{ + "schema_version": 1, + "overall_status": "scanning", + "components": [ + { + "component_id": "desktop-runtime", + "label": "Desktop Runtime", + "status": "scanning", + "required": true, + "version": null, + "path": null, + "error_code": null, + "remediation": null, + "detail": null + }, + { + "component_id": "data-root", + "label": "Local Data Root", + "status": "scanning", + "required": true, + "version": null, + "path": null, + "error_code": null, + "remediation": null, + "detail": null + }, + { + "component_id": "bundled-uv", + "label": "uv Runtime Manager", + "status": "scanning", + "required": true, + "version": null, + "path": null, + "error_code": null, + "remediation": null, + "detail": null + }, + { + "component_id": "python-environment", + "label": "Python Environment", + "status": "scanning", + "required": true, + "version": null, + "path": null, + "error_code": null, + "remediation": null, + "detail": null + }, + { + "component_id": "ffmpeg", + "label": "FFmpeg", + "status": "scanning", + "required": true, + "version": null, + "path": null, + "error_code": null, + "remediation": null, + "detail": null + }, + { + "component_id": "ffprobe", + "label": "FFprobe", + "status": "scanning", + "required": true, + "version": null, + "path": null, + "error_code": null, + "remediation": null, + "detail": null + }, + { + "component_id": "desktop-proxy-port", + "label": "Desktop Proxy Port", + "status": "scanning", + "required": true, + "version": null, + "path": null, + "error_code": null, + "remediation": null, + "detail": null + }, + { + "component_id": "backend-private-port", + "label": "Backend Private Port", + "status": "scanning", + "required": true, + "version": null, + "path": null, + "error_code": null, + "remediation": null, + "detail": null + }, + { + "component_id": "runtime-settings", + "label": "Runtime Settings", + "status": "scanning", + "required": true, + "version": null, + "path": null, + "error_code": null, + "remediation": null, + "detail": null + }, + { + "component_id": "webview2", + "label": "Microsoft Edge WebView2", + "status": "scanning", + "required": true, + "version": null, + "path": null, + "error_code": null, + "remediation": null, + "detail": null + } + ] +} diff --git a/web/src/lib/tauri.test.ts b/web/src/lib/tauri.test.ts index 6a22ca6..b05b0ca 100644 --- a/web/src/lib/tauri.test.ts +++ b/web/src/lib/tauri.test.ts @@ -1,7 +1,12 @@ /** @vitest-environment jsdom */ import { describe, expect, it } from "vitest" -import { normalizeBootstrapStatus, type BackendStatus } from "@/lib/tauri" +import { + normalizeBootstrapPreflight, + normalizeBootstrapStatus, + type BackendStatus, +} from "@/lib/tauri" +import scanningPreflight from "@/lib/fixtures/bootstrap-preflight-scanning-v1.json" function backendStatus(overrides: Partial = {}): BackendStatus { return { @@ -49,3 +54,100 @@ describe("normalizeBootstrapStatus", () => { expect(normalizeBootstrapStatus(backendStatus({ state: "error" })).phase).toBe("FAILED_RETRYABLE") }) }) + +describe("normalizeBootstrapPreflight", () => { + const componentIds = [ + "desktop-runtime", + "data-root", + "bundled-uv", + "python-environment", + "ffmpeg", + "ffprobe", + "desktop-proxy-port", + "backend-private-port", + "runtime-settings", + "webview2", + ] + const validPreflight = { + schema_version: 1, + overall_status: "needs_configuration", + components: componentIds.map((componentId) => ({ + component_id: componentId, + label: componentId, + status: componentId === "runtime-settings" ? "invalid" : "ready", + required: true, + version: null, + path: null, + error_code: componentId === "runtime-settings" ? "CONFIG_INVALID" : null, + remediation: componentId === "runtime-settings" ? "Repair the settings file." : null, + detail: null, + })), + } + + it("accepts schema v1 and preserves nullable component fields", () => { + expect(normalizeBootstrapPreflight(validPreflight)).toEqual(validPreflight) + }) + + it("accepts the native scanning-state contract", () => { + expect(normalizeBootstrapPreflight(scanningPreflight)).toEqual(scanningPreflight) + }) + + it.each([ + null, + { ...validPreflight, schema_version: 2 }, + { ...validPreflight, overall_status: "future_status" }, + { ...validPreflight, components: null }, + { ...validPreflight, components: validPreflight.components.slice(0, -1) }, + { + ...validPreflight, + components: [...validPreflight.components, validPreflight.components[0]], + }, + { + ...validPreflight, + components: [ + validPreflight.components[1], + validPreflight.components[0], + ...validPreflight.components.slice(2), + ], + }, + { + ...validPreflight, + components: validPreflight.components.map((component, index) => ( + index === 0 ? { ...component, component_id: "future-component" } : component + )), + }, + { + ...validPreflight, + components: validPreflight.components.map((component, index) => ( + index === 0 + ? { + ...component, + status: "future_status", + } + : component + )), + }, + { + ...validPreflight, + components: validPreflight.components.map((component, index) => ( + index === 0 ? { ...component, detail: 42 } : component + )), + }, + { + ...validPreflight, + components: validPreflight.components.map((component, index) => ( + index === 0 ? { ...component, unexpected: "field" } : component + )), + }, + { + ...validPreflight, + components: validPreflight.components.map((component, index) => ( + index === 1 ? validPreflight.components[0] : component + )), + }, + ])("rejects incompatible or malformed IPC payload %#", (payload) => { + expect(() => normalizeBootstrapPreflight(payload)).toThrow( + "Desktop preflight response has an incompatible schema.", + ) + }) +}) diff --git a/web/src/lib/tauri.ts b/web/src/lib/tauri.ts index d2e9dcb..d74bc50 100644 --- a/web/src/lib/tauri.ts +++ b/web/src/lib/tauri.ts @@ -58,8 +58,183 @@ export interface BootstrapDiagnostics { logs: BackendLogEntry[] } +export type BootstrapPreflightOverallStatus = + | "scanning" + | "ready" + | "needs_configuration" + | "needs_repair" + | "blocked" + +export type BootstrapPreflightComponentStatus = + | "scanning" + | "ready" + | "warning" + | "missing" + | "invalid" + | "blocked" + +const BOOTSTRAP_PREFLIGHT_OVERALL_STATUSES = [ + "scanning", + "ready", + "needs_configuration", + "needs_repair", + "blocked", +] as const satisfies readonly BootstrapPreflightOverallStatus[] + +const BOOTSTRAP_PREFLIGHT_COMPONENT_STATUSES = [ + "scanning", + "ready", + "warning", + "missing", + "invalid", + "blocked", +] as const satisfies readonly BootstrapPreflightComponentStatus[] + +const BOOTSTRAP_PREFLIGHT_COMPONENT_IDS = [ + "desktop-runtime", + "data-root", + "bundled-uv", + "python-environment", + "ffmpeg", + "ffprobe", + "desktop-proxy-port", + "backend-private-port", + "runtime-settings", + "webview2", +] as const + +export interface BootstrapPreflightComponent { + component_id: string + label: string + status: BootstrapPreflightComponentStatus + required: boolean + version: string | null + path: string | null + error_code: string | null + remediation: string | null + detail: string | null +} + +export interface BootstrapPreflight { + schema_version: 1 + overall_status: BootstrapPreflightOverallStatus + components: BootstrapPreflightComponent[] +} + +const PREFLIGHT_REPORT_FIELDS = [ + "schema_version", + "overall_status", + "components", +] as const + +const PREFLIGHT_COMPONENT_FIELDS = [ + "component_id", + "label", + "status", + "required", + "version", + "path", + "error_code", + "remediation", + "detail", +] as const + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value) +} + +function hasExactFields( + value: Record, + expectedFields: readonly string[], +) { + const actualFields = Object.keys(value).sort() + const sortedExpectedFields = [...expectedFields].sort() + return ( + actualFields.length === sortedExpectedFields.length + && actualFields.every((field, index) => field === sortedExpectedFields[index]) + ) +} + +function isNullableString(value: unknown): value is string | null { + return value === null || typeof value === "string" +} + +function isBootstrapPreflightOverallStatus( + value: unknown, +): value is BootstrapPreflightOverallStatus { + return ( + typeof value === "string" + && BOOTSTRAP_PREFLIGHT_OVERALL_STATUSES.some((status) => status === value) + ) +} + +function isBootstrapPreflightComponentStatus( + value: unknown, +): value is BootstrapPreflightComponentStatus { + return ( + typeof value === "string" + && BOOTSTRAP_PREFLIGHT_COMPONENT_STATUSES.some((status) => status === value) + ) +} + +function invalidPreflightSchema(): never { + throw new Error("Desktop preflight response has an incompatible schema.") +} + +export function normalizeBootstrapPreflight(value: unknown): BootstrapPreflight { + if ( + !isRecord(value) + || !hasExactFields(value, PREFLIGHT_REPORT_FIELDS) + || value.schema_version !== 1 + || !isBootstrapPreflightOverallStatus(value.overall_status) + || !Array.isArray(value.components) + || value.components.length !== BOOTSTRAP_PREFLIGHT_COMPONENT_IDS.length + ) { + return invalidPreflightSchema() + } + + const components = value.components.map((component, index) => { + if ( + !isRecord(component) + || !hasExactFields(component, PREFLIGHT_COMPONENT_FIELDS) + || typeof component.component_id !== "string" + || component.component_id !== BOOTSTRAP_PREFLIGHT_COMPONENT_IDS[index] + || typeof component.label !== "string" + || component.label.length === 0 + || !isBootstrapPreflightComponentStatus(component.status) + || typeof component.required !== "boolean" + || !isNullableString(component.version) + || !isNullableString(component.path) + || !isNullableString(component.error_code) + || !isNullableString(component.remediation) + || !isNullableString(component.detail) + ) { + return invalidPreflightSchema() + } + + return { + component_id: component.component_id, + label: component.label, + status: component.status, + required: component.required, + version: component.version, + path: component.path, + error_code: component.error_code, + remediation: component.remediation, + detail: component.detail, + } + }) + + return { + schema_version: 1, + overall_status: value.overall_status, + components, + } +} + export interface BootstrapBridge { getStatus(): Promise + getPreflight(): Promise retry(): Promise openLogs(): Promise getDiagnostics(): Promise @@ -245,6 +420,9 @@ export function getBootstrapBridge(): BootstrapBridge | undefined { tauriBootstrapBridge = { getStatus, + getPreflight: async () => normalizeBootstrapPreflight( + await tauriInvoke("bootstrap_get_preflight"), + ), retry: async () => normalizeBootstrapStatus(await tauriInvoke("backend_retry")), openLogs: () => tauriInvoke("open_logs"), async getDiagnostics() { From 95fa2352dec895a14fa74b7686e8c89a525bc6b0 Mon Sep 17 00:00:00 2001 From: cmander02 <15946840899@163.com> Date: Tue, 28 Jul 2026 21:32:58 +0800 Subject: [PATCH 21/25] fix(desktop): ship runtime with portable executable --- .gitignore | 5 ++ README.md | 3 +- scripts/build-desktop.ps1 | 113 +++++++++++++++++++++++-- start-tauri.bat | 12 ++- tests/test_desktop_handoff_contract.py | 37 ++++++++ web/src-tauri/src/main.rs | 35 ++++++-- 6 files changed, 184 insertions(+), 21 deletions(-) create mode 100644 tests/test_desktop_handoff_contract.py diff --git a/.gitignore b/.gitignore index b345236..979b52e 100644 --- a/.gitignore +++ b/.gitignore @@ -86,6 +86,11 @@ tmp-electron-run/ release/ MPP.exe MPP.exe.attestation.json +/MPP.exe.next +/MPP.exe.previous +/runtime/ +/runtime.next/ +/runtime.previous/ # Generated desktop runtime staging (assembled from a strict release whitelist) /web/src-tauri/resources/runtime/* diff --git a/README.md b/README.md index ea3ace0..2dd7185 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,8 @@ cd web && npm install && npm run dev cd web && npm run tauri:build ``` -构建成功后会将最新桌面程序复制到项目根目录的 `MPP.exe`,后续构建直接替换该文件。 +构建成功后会将同一次构建产生的 `MPP.exe` 与 `runtime/` 复制到项目根目录。 +两者共同组成便携版桌面程序;使用 `start-tauri.bat` 启动时会先检查运行时清单。 **方式 2: 后端 + 浏览器** diff --git a/scripts/build-desktop.ps1 b/scripts/build-desktop.ps1 index ccd797e..d38299e 100644 --- a/scripts/build-desktop.ps1 +++ b/scripts/build-desktop.ps1 @@ -14,7 +14,11 @@ $RuntimeRoot = Join-Path $TauriRoot "resources\runtime" $Profile = if ($Debug) { "debug" } else { "release" } $SourceExe = Join-Path $TauriRoot "target\$Profile\mpp-desktop.exe" $DestinationExe = Join-Path $ProjectRoot "MPP.exe" +$DestinationRuntime = Join-Path $ProjectRoot "runtime" $TemporaryExe = Join-Path $ProjectRoot "MPP.exe.next" +$TemporaryRuntime = Join-Path $ProjectRoot "runtime.next" +$PreviousExe = Join-Path $ProjectRoot "MPP.exe.previous" +$PreviousRuntime = Join-Path $ProjectRoot "runtime.previous" $AttestationFile = Join-Path $ProjectRoot "MPP.exe.attestation.json" $ReleaseLockPath = Join-Path $TauriRoot "resources\.release-build.lock" $PrepareStampPath = Join-Path $WebRoot "node_modules\.mpp-release-prepare.json" @@ -263,7 +267,8 @@ function Invoke-RuntimeVerification { [Parameter(Mandatory = $true)][string]$PythonPath, [Parameter(Mandatory = $true)][string]$SourceCommit, [Parameter(Mandatory = $true)][string]$Version, - [Parameter(Mandatory = $true)][string]$BuildInputDigest + [Parameter(Mandatory = $true)][string]$BuildInputDigest, + [string]$RuntimePath = $RuntimeRoot ) [void](Invoke-NativeText -Command $PythonPath -Arguments @( @@ -271,7 +276,7 @@ function Invoke-RuntimeVerification { "-S", "-B", $VerifierPath, - $RuntimeRoot, + $RuntimePath, "--verify-tools", "--expected-source-commit", $SourceCommit, @@ -354,11 +359,50 @@ function Assert-PrepareStamp { } } -function Install-RootExecutable { +function Install-RootArtifacts { + param( + [Parameter(Mandatory = $true)][string]$PythonPath, + [Parameter(Mandatory = $true)][string]$SourceCommit, + [Parameter(Mandatory = $true)][string]$Version, + [Parameter(Mandatory = $true)][string]$BuildInputDigest, + [Parameter(Mandatory = $true)][string]$ManifestHash + ) + if (-not (Test-Path -LiteralPath $SourceExe -PathType Leaf)) { throw "Built executable was not found: $SourceExe" } + if (-not (Test-Path -LiteralPath $RuntimeRoot -PathType Container)) { + throw "Built runtime was not found: $RuntimeRoot" + } + foreach ($BackupPath in @($PreviousExe, $PreviousRuntime)) { + if (Test-Path -LiteralPath $BackupPath) { + throw ( + "A previous desktop handoff backup requires inspection before rebuilding: " + + $BackupPath + ) + } + } + foreach ($StagingPath in @($TemporaryExe, $TemporaryRuntime)) { + if (Test-Path -LiteralPath $StagingPath) { + Remove-Item -LiteralPath $StagingPath -Recurse -Force + } + } + Copy-Item -LiteralPath $SourceExe -Destination $TemporaryExe -Force + Copy-Item -LiteralPath $RuntimeRoot -Destination $TemporaryRuntime -Recurse + Invoke-RuntimeVerification -PythonPath $PythonPath ` + -SourceCommit $SourceCommit ` + -Version $Version ` + -BuildInputDigest $BuildInputDigest ` + -RuntimePath $TemporaryRuntime + $StagedManifestHash = ( + Get-FileHash -LiteralPath ( + Join-Path $TemporaryRuntime "runtime-manifest.json" + ) -Algorithm SHA256 + ).Hash.ToLowerInvariant() + if ($StagedManifestHash -ne $ManifestHash) { + throw "Root runtime staging changed the attested runtime manifest" + } $DestinationFullPath = [System.IO.Path]::GetFullPath($DestinationExe) $RunningLaunchers = Get-CimInstance -ClassName Win32_Process -ErrorAction SilentlyContinue | @@ -372,10 +416,52 @@ function Install-RootExecutable { Wait-Process -Id $Launcher.ProcessId -Timeout 10 -ErrorAction SilentlyContinue } - if (Test-Path -LiteralPath $DestinationExe) { - Remove-Item -LiteralPath $DestinationExe -Force + $InstalledRuntime = $false + $InstalledExe = $false + try { + if (Test-Path -LiteralPath $DestinationExe) { + Move-Item -LiteralPath $DestinationExe -Destination $PreviousExe + } + if (Test-Path -LiteralPath $DestinationRuntime) { + Move-Item -LiteralPath $DestinationRuntime -Destination $PreviousRuntime + } + Move-Item -LiteralPath $TemporaryRuntime -Destination $DestinationRuntime + $InstalledRuntime = $true + Move-Item -LiteralPath $TemporaryExe -Destination $DestinationExe + $InstalledExe = $true + } catch { + $InstallError = $_ + try { + if ($InstalledExe -and (Test-Path -LiteralPath $DestinationExe)) { + Remove-Item -LiteralPath $DestinationExe -Force + } + if ( + $InstalledRuntime -and + (Test-Path -LiteralPath $DestinationRuntime) + ) { + Remove-Item -LiteralPath $DestinationRuntime -Recurse -Force + } + if (Test-Path -LiteralPath $PreviousRuntime) { + Move-Item -LiteralPath $PreviousRuntime -Destination $DestinationRuntime + } + if (Test-Path -LiteralPath $PreviousExe) { + Move-Item -LiteralPath $PreviousExe -Destination $DestinationExe + } + } catch { + throw ( + "Desktop artifact handoff failed and rollback also failed. " + + "Handoff error: $($InstallError.Exception.Message) " + + "Rollback error: $($_.Exception.Message)" + ) + } + throw $InstallError + } + + foreach ($BackupPath in @($PreviousExe, $PreviousRuntime)) { + if (Test-Path -LiteralPath $BackupPath) { + Remove-Item -LiteralPath $BackupPath -Recurse -Force + } } - Move-Item -LiteralPath $TemporaryExe -Destination $DestinationExe } $ReleaseLock = $null @@ -504,7 +590,11 @@ try { throw "Built executable does not contain the attested runtime manifest identity" } - Install-RootExecutable + Install-RootArtifacts -PythonPath $PythonPath ` + -SourceCommit $SourceCommit ` + -Version $Version ` + -BuildInputDigest $BuildInputDigest ` + -ManifestHash $FinalManifestHash $Artifact = Get-Item -LiteralPath $DestinationExe $ArtifactHash = ( Get-FileHash -LiteralPath $DestinationExe -Algorithm SHA256 @@ -521,6 +611,10 @@ try { buildInputDigest = $BuildInputDigest runtimeManifestSha256 = $FinalManifestHash webIndexSha256 = $WebIndexHash + runtime = [ordered]@{ + path = "runtime" + manifest = "runtime/runtime-manifest.json" + } artifact = [ordered]@{ path = "MPP.exe" size = $Artifact.Length @@ -544,7 +638,7 @@ try { throw "Release source identity changed during artifact handoff" } - Write-Host "[OK] Root executable updated: $($Artifact.FullName)" + Write-Host "[OK] Root desktop artifacts updated: $($Artifact.FullName) + $DestinationRuntime" Write-Host "[OK] Size: $($Artifact.Length) bytes" Write-Host "[OK] SHA-256: $ArtifactHash" Write-Host "[OK] Attestation: $AttestationFile" @@ -561,4 +655,7 @@ try { if (Test-Path -LiteralPath $TemporaryExe) { Remove-Item -LiteralPath $TemporaryExe -Force } + if (Test-Path -LiteralPath $TemporaryRuntime) { + Remove-Item -LiteralPath $TemporaryRuntime -Recurse -Force + } } diff --git a/start-tauri.bat b/start-tauri.bat index ed78faa..5716204 100644 --- a/start-tauri.bat +++ b/start-tauri.bat @@ -4,14 +4,20 @@ title MediaProcessPipeline Tauri cd /d "%~dp0" +set "BUILD_LOCK=%~dp0web\src-tauri\resources\.release-build.lock" set "APP_EXE=%~dp0MPP.exe" -if not exist "%APP_EXE%" set "APP_EXE=%~dp0web\src-tauri\target\release\mpp-desktop.exe" +set "APP_RUNTIME_MANIFEST=%~dp0runtime\runtime-manifest.json" -if exist "%APP_EXE%" ( +if exist "%BUILD_LOCK%" ( + echo Desktop build is in progress. Start MPP again after it completes. + exit /b 1 +) + +if exist "%APP_EXE%" if exist "%APP_RUNTIME_MANIFEST%" ( start "" "%APP_EXE%" exit /b 0 ) -echo Tauri executable not found. +echo Desktop artifacts are incomplete. MPP.exe and runtime\runtime-manifest.json are required. echo Build it with: cd web ^&^& npm run tauri:build:portable exit /b 1 diff --git a/tests/test_desktop_handoff_contract.py b/tests/test_desktop_handoff_contract.py new file mode 100644 index 0000000..65a4f50 --- /dev/null +++ b/tests/test_desktop_handoff_contract.py @@ -0,0 +1,37 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +def test_desktop_build_hands_off_executable_and_matching_runtime(): + script = (ROOT / "scripts" / "build-desktop.ps1").read_text(encoding="utf-8") + + assert '$DestinationRuntime = Join-Path $ProjectRoot "runtime"' in script + assert '$TemporaryRuntime = Join-Path $ProjectRoot "runtime.next"' in script + assert ( + "Copy-Item -LiteralPath $RuntimeRoot -Destination $TemporaryRuntime -Recurse" + in script + ) + assert "-RuntimePath $TemporaryRuntime" in script + assert "Root runtime staging changed the attested runtime manifest" in script + assert ( + "Move-Item -LiteralPath $TemporaryRuntime -Destination $DestinationRuntime" + in script + ) + assert ( + "Move-Item -LiteralPath $PreviousRuntime -Destination $DestinationRuntime" + in script + ) + assert "$InstalledRuntime -and" in script + assert "$InstalledExe -and" in script + + +def test_root_launcher_requires_complete_desktop_artifact_pair(): + launcher = (ROOT / "start-tauri.bat").read_text(encoding="utf-8") + + assert r"runtime\runtime-manifest.json" in launcher + assert r"resources\.release-build.lock" in launcher + assert ( + 'if exist "%APP_EXE%" if exist "%APP_RUNTIME_MANIFEST%" (' in launcher + ) diff --git a/web/src-tauri/src/main.rs b/web/src-tauri/src/main.rs index 942bf0e..a0235e7 100644 --- a/web/src-tauri/src/main.rs +++ b/web/src-tauri/src/main.rs @@ -851,17 +851,13 @@ fn validate_existing_path_chain(path: &Path, label: &str) -> Result<(), String> if !path.is_absolute() { return Err(format!("{label} must be absolute: {}", path.display())); } - let mut current = PathBuf::new(); - for component in path.components() { - current.push(component.as_os_str()); - if !current.is_absolute() { - continue; - } - match fs::symlink_metadata(¤t) { + let ancestors = path.ancestors().collect::>(); + for ancestor in ancestors.into_iter().rev() { + match fs::symlink_metadata(ancestor) { Ok(metadata) if metadata_is_link_or_reparse(&metadata) => { return Err(format!( "{label} contains a symlink, junction, or reparse point at {}", - current.display() + ancestor.display() )); } Ok(_) => {} @@ -869,7 +865,7 @@ fn validate_existing_path_chain(path: &Path, label: &str) -> Result<(), String> Err(error) => { return Err(format!( "cannot inspect {label} ancestor {}: {error}", - current.display() + ancestor.display() )); } } @@ -7091,6 +7087,27 @@ mod tests { assert!(error.contains("failed to create") || error.contains("real directory")); } + #[cfg(windows)] + #[test] + fn existing_path_chain_accepts_a_windows_verbatim_disk_path() { + let temporary = TestDirectory::new("verbatim-path-chain"); + let verbatim_root = + fs::canonicalize(&temporary.path).expect("temporary path should canonicalize"); + assert!( + matches!( + verbatim_root.components().next(), + Some(std::path::Component::Prefix(prefix)) + if matches!(prefix.kind(), std::path::Prefix::VerbatimDisk(_)) + ), + "fixture should use a verbatim disk path: {}", + verbatim_root.display() + ); + + let future_path = verbatim_root.join("runtime").join("future"); + validate_existing_path_chain(&future_path, "verbatim runtime path") + .expect("verbatim path ancestors should be inspectable"); + } + #[cfg(windows)] #[test] fn user_layout_rejects_a_windows_junction() { From 2c41cd88828b15b384411dfb2ef88d9f40f60a2e Mon Sep 17 00:00:00 2001 From: cmander02 <15946840899@163.com> Date: Tue, 28 Jul 2026 21:51:05 +0800 Subject: [PATCH 22/25] style(tests): format desktop handoff contract --- tests/test_desktop_handoff_contract.py | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/tests/test_desktop_handoff_contract.py b/tests/test_desktop_handoff_contract.py index 65a4f50..cffa907 100644 --- a/tests/test_desktop_handoff_contract.py +++ b/tests/test_desktop_handoff_contract.py @@ -1,6 +1,5 @@ from pathlib import Path - ROOT = Path(__file__).resolve().parents[1] @@ -9,20 +8,11 @@ def test_desktop_build_hands_off_executable_and_matching_runtime(): assert '$DestinationRuntime = Join-Path $ProjectRoot "runtime"' in script assert '$TemporaryRuntime = Join-Path $ProjectRoot "runtime.next"' in script - assert ( - "Copy-Item -LiteralPath $RuntimeRoot -Destination $TemporaryRuntime -Recurse" - in script - ) + assert "Copy-Item -LiteralPath $RuntimeRoot -Destination $TemporaryRuntime -Recurse" in script assert "-RuntimePath $TemporaryRuntime" in script assert "Root runtime staging changed the attested runtime manifest" in script - assert ( - "Move-Item -LiteralPath $TemporaryRuntime -Destination $DestinationRuntime" - in script - ) - assert ( - "Move-Item -LiteralPath $PreviousRuntime -Destination $DestinationRuntime" - in script - ) + assert "Move-Item -LiteralPath $TemporaryRuntime -Destination $DestinationRuntime" in script + assert "Move-Item -LiteralPath $PreviousRuntime -Destination $DestinationRuntime" in script assert "$InstalledRuntime -and" in script assert "$InstalledExe -and" in script @@ -32,6 +22,4 @@ def test_root_launcher_requires_complete_desktop_artifact_pair(): assert r"runtime\runtime-manifest.json" in launcher assert r"resources\.release-build.lock" in launcher - assert ( - 'if exist "%APP_EXE%" if exist "%APP_RUNTIME_MANIFEST%" (' in launcher - ) + assert 'if exist "%APP_EXE%" if exist "%APP_RUNTIME_MANIFEST%" (' in launcher From cba882ef0bc510c8c2b02a70e00ed44e518dc420 Mon Sep 17 00:00:00 2001 From: cmander02 <15946840899@163.com> Date: Tue, 28 Jul 2026 22:27:21 +0800 Subject: [PATCH 23/25] fix(desktop): start installed backend without uv resync --- web/src-tauri/src/main.rs | 101 +++++++++++++------------------------- 1 file changed, 33 insertions(+), 68 deletions(-) diff --git a/web/src-tauri/src/main.rs b/web/src-tauri/src/main.rs index a0235e7..a1ba4fa 100644 --- a/web/src-tauri/src/main.rs +++ b/web/src-tauri/src/main.rs @@ -45,7 +45,7 @@ const PROXY_CLIENT_BODY_IDLE_TIMEOUT: Duration = Duration::from_secs(30); const PROXY_BACKEND_CONNECT_TIMEOUT: Duration = Duration::from_millis(500); const PROXY_BACKEND_HEALTH_TIMEOUT: Duration = Duration::from_secs(2); const BACKEND_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(8); -const BACKEND_COMMAND: &str = "uv run python -u -m app.cli serve --desktop-loopback"; +const BACKEND_COMMAND: &str = "python -u -m app.cli serve --desktop-loopback"; const MAX_LOG_LINES: usize = 1200; const MAX_LOG_LINE_BYTES: usize = 16 * 1024; const PROCESS_FORCE_TIMEOUT: Duration = Duration::from_secs(3); @@ -1643,18 +1643,22 @@ where } fn build_backend_launch_spec(layout: &RuntimeLayout) -> BackendLaunchSpec { - let mut args = vec![OsString::from("run")]; - if layout.mode == RuntimeMode::Installed { - args.push(OsString::from("--frozen")); - } - args.extend([ - OsString::from("--project"), - layout.runtime_root.as_os_str().to_os_string(), - OsString::from("python"), - ]); - if layout.mode == RuntimeMode::Installed { - args.extend([OsString::from("-E"), OsString::from("-s")]); - } + let installed = layout.mode == RuntimeMode::Installed; + let program = if installed { + python_venv_executable(&layout.user.venv_dir).into_os_string() + } else { + layout.uv_executable.clone() + }; + let mut args = if installed { + vec![OsString::from("-E"), OsString::from("-s")] + } else { + vec![ + OsString::from("run"), + OsString::from("--project"), + layout.runtime_root.as_os_str().to_os_string(), + OsString::from("python"), + ] + }; args.extend([ OsString::from("-u"), OsString::from("-m"), @@ -1665,7 +1669,7 @@ fn build_backend_launch_spec(layout: &RuntimeLayout) -> BackendLaunchSpec { OsString::from(layout.backend_port.to_string()), ]); - let mut command_env = if layout.mode == RuntimeMode::Installed { + let mut command_env = if installed { safe_inherited_environment(env::vars_os()) } else { BTreeMap::new() @@ -1686,7 +1690,7 @@ fn build_backend_launch_spec(layout: &RuntimeLayout) -> BackendLaunchSpec { ("MPP_SKIP_VERSION_CHECK".to_string(), OsString::from("1")), ]); - if layout.mode == RuntimeMode::Installed { + if installed { command_env.extend([ ( "MPP_CONFIG_FILE".to_string(), @@ -1702,19 +1706,6 @@ fn build_backend_launch_spec(layout: &RuntimeLayout) -> BackendLaunchSpec { path_env(&layout.web_dist_dir), ), ("MPP_DATA_ROOT".to_string(), path_env(&layout.user.data_dir)), - ( - "UV_PROJECT_ENVIRONMENT".to_string(), - path_env(&layout.user.venv_dir), - ), - ( - "UV_CACHE_DIR".to_string(), - path_env(&layout.user.cache_dir.join("uv")), - ), - ( - "UV_PYTHON_INSTALL_DIR".to_string(), - path_env(&layout.user.python_dir), - ), - ("UV_MANAGED_PYTHON".to_string(), OsString::from("1")), ( "HF_HOME".to_string(), path_env(&layout.user.cache_dir.join("huggingface")), @@ -1732,17 +1723,15 @@ fn build_backend_launch_spec(layout: &RuntimeLayout) -> BackendLaunchSpec { ("PYTHONSAFEPATH".to_string(), OsString::from("1")), ("TEMP".to_string(), path_env(&layout.user.temp_dir)), ("TMP".to_string(), path_env(&layout.user.temp_dir)), - ("UV_LINK_MODE".to_string(), OsString::from("copy")), - ("UV_NO_CONFIG".to_string(), OsString::from("1")), ]); } BackendLaunchSpec { - program: layout.uv_executable.clone(), + program, cwd: layout.backend_dir.clone(), args, env: command_env, - clear_environment: layout.mode == RuntimeMode::Installed, + clear_environment: installed, } } @@ -7155,7 +7144,7 @@ mod tests { } #[test] - fn installed_launch_uses_bundled_uv_frozen_project_and_user_env() { + fn installed_launch_uses_user_venv_python_and_isolated_environment() { let temporary = TestDirectory::new("installed-command"); let runtime_root = temporary.path.join("Program Files/MPP/resources/runtime"); let user_root = temporary.path.join("Local AppData/MediaProcessPipeline"); @@ -7176,26 +7165,14 @@ mod tests { assert!(spec.clear_environment); assert_eq!( PathBuf::from(&spec.program), - runtime_root.join("bin/uv.exe") + user_root + .join("runtime") + .join(".venv") + .join("Scripts") + .join("python.exe") ); assert_eq!(spec.cwd, runtime_root.join("backend")); - assert_eq!( - &args[..4], - &[ - "run", - "--frozen", - "--project", - runtime_root.to_string_lossy().as_ref() - ] - ); - let python_index = args - .iter() - .position(|argument| argument == "python") - .expect("launch should invoke Python"); - assert_eq!( - &args[python_index..python_index + 4], - &["python", "-E", "-s", "-u"] - ); + assert_eq!(&args[..3], &["-E", "-s", "-u"]); assert!(args.iter().any(|argument| argument == "--desktop-loopback")); assert_eq!( spec.env.get("MPP_CONFIG_FILE"), @@ -7217,22 +7194,10 @@ mod tests { spec.env.get("MPP_DATA_ROOT"), Some(&path_env(&user_root.join("data"))) ); - assert_eq!( - spec.env.get("UV_PROJECT_ENVIRONMENT"), - Some(&path_env(&user_root.join("runtime").join(".venv"))) - ); - assert_eq!( - spec.env.get("UV_CACHE_DIR"), - Some(&path_env(&user_root.join("cache").join("uv"))) - ); - assert_eq!( - spec.env.get("UV_PYTHON_INSTALL_DIR"), - Some(&path_env(&user_root.join("runtime").join("python"))) - ); - assert_eq!( - spec.env.get("UV_MANAGED_PYTHON"), - Some(&OsString::from("1")) - ); + assert!(!spec.env.contains_key("UV_PROJECT_ENVIRONMENT")); + assert!(!spec.env.contains_key("UV_CACHE_DIR")); + assert!(!spec.env.contains_key("UV_PYTHON_INSTALL_DIR")); + assert!(!spec.env.contains_key("UV_MANAGED_PYTHON")); assert_eq!( spec.env.get("HF_HOME"), Some(&path_env(&user_root.join("cache").join("huggingface"))) @@ -7263,7 +7228,7 @@ mod tests { spec.env.get("TMP"), Some(&path_env(&user_root.join("runtime").join("tmp"))) ); - assert_eq!(spec.env.get("UV_NO_CONFIG"), Some(&OsString::from("1"))); + assert!(!spec.env.contains_key("UV_NO_CONFIG")); } #[test] From 63c6f944f71663bc766311a27326424f5cf3d2d2 Mon Sep 17 00:00:00 2001 From: cmander02 <15946840899@163.com> Date: Tue, 28 Jul 2026 22:44:37 +0800 Subject: [PATCH 24/25] fix(desktop): keep installed runtime immutable --- web/src-tauri/src/main.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/web/src-tauri/src/main.rs b/web/src-tauri/src/main.rs index a1ba4fa..e9b344e 100644 --- a/web/src-tauri/src/main.rs +++ b/web/src-tauri/src/main.rs @@ -1650,7 +1650,11 @@ fn build_backend_launch_spec(layout: &RuntimeLayout) -> BackendLaunchSpec { layout.uv_executable.clone() }; let mut args = if installed { - vec![OsString::from("-E"), OsString::from("-s")] + vec![ + OsString::from("-E"), + OsString::from("-s"), + OsString::from("-B"), + ] } else { vec![ OsString::from("run"), @@ -7172,7 +7176,7 @@ mod tests { .join("python.exe") ); assert_eq!(spec.cwd, runtime_root.join("backend")); - assert_eq!(&args[..3], &["-E", "-s", "-u"]); + assert_eq!(&args[..4], &["-E", "-s", "-B", "-u"]); assert!(args.iter().any(|argument| argument == "--desktop-loopback")); assert_eq!( spec.env.get("MPP_CONFIG_FILE"), From 3320a950b2de1b2ef63a8fba38055557ec993155 Mon Sep 17 00:00:00 2001 From: cmander02 <15946840899@163.com> Date: Tue, 28 Jul 2026 23:33:52 +0800 Subject: [PATCH 25/25] fix(desktop): restore local settings access --- web/src-tauri/src/main.rs | 47 ++++++++++--- web/src/components/settings-panel.test.tsx | 20 ++++++ web/src/components/settings-panel.tsx | 82 +++++++++++++++++----- 3 files changed, 121 insertions(+), 28 deletions(-) diff --git a/web/src-tauri/src/main.rs b/web/src-tauri/src/main.rs index e9b344e..acd4733 100644 --- a/web/src-tauri/src/main.rs +++ b/web/src-tauri/src/main.rs @@ -35,6 +35,10 @@ const BACKEND_PORT: u16 = 18000; const APP_URL: &str = "http://localhost:18000"; const DESKTOP_API_HOST: &str = "api.tauri.localhost"; const DESKTOP_PROXY_COOKIE: &str = "mpp_desktop_proxy"; +#[cfg(target_os = "windows")] +const WEBVIEW2_BROWSER_ARGUMENTS: &str = + "--disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection \ + --proxy-bypass-list=api.tauri.localhost"; const MAX_PROXY_CONNECTIONS: usize = 128; const MAX_HTTP_HEAD_BYTES: usize = 64 * 1024; const MAX_HEALTH_RESPONSE_BYTES: usize = 64 * 1024; @@ -5391,6 +5395,10 @@ fn classify_settings_preflight_output(output: &ProbeCommandOutput) -> SettingsPr } } +fn settings_preflight_arguments<'a>(helper: &'a str, config: &'a str) -> [&'a str; 4] { + ["-I", "-B", helper, config] +} + fn probe_runtime_settings_component(layout: &RuntimeLayout) -> BootstrapPreflightComponent { let required = layout.mode == RuntimeMode::Installed; let path = &layout.user.config_file; @@ -5510,7 +5518,8 @@ fn probe_runtime_settings_component(layout: &RuntimeLayout) -> BootstrapPrefligh let helper_argument = helper.to_string_lossy().into_owned(); let config_argument = path.to_string_lossy().into_owned(); - let arguments = ["-I", helper_argument.as_str(), config_argument.as_str()]; + let arguments = + settings_preflight_arguments(helper_argument.as_str(), config_argument.as_str()); let output = match run_bounded_probe_command(&python, &arguments, PREFLIGHT_PROCESS_TIMEOUT) { Ok(output) => output, Err(failure) => { @@ -6466,15 +6475,19 @@ fn setup_app(app: &mut tauri::App) -> Result<(), Box> { app.manage(BackendProcess::default()); app.manage(BootstrapController::new(fallback_log_dir)); let bootstrap_url = packaged_bootstrap_navigation_url().map_err(boxed_error)?; - let _window = WebviewWindowBuilder::new(app, "main", WebviewUrl::External(bootstrap_url)) - .on_navigation(trusted_app_navigation) - .on_new_window(|url, _features| { - let _ = open_external_url(url.to_string()); - NewWindowResponse::Deny - }) - .title("MediaProcessPipeline") - .inner_size(1440.0, 980.0) - .min_inner_size(1024.0, 720.0) + let window_builder = + WebviewWindowBuilder::new(app, "main", WebviewUrl::External(bootstrap_url)) + .on_navigation(trusted_app_navigation) + .on_new_window(|url, _features| { + let _ = open_external_url(url.to_string()); + NewWindowResponse::Deny + }) + .title("MediaProcessPipeline") + .inner_size(1440.0, 980.0) + .min_inner_size(1024.0, 720.0); + #[cfg(target_os = "windows")] + let window_builder = window_builder.additional_browser_args(WEBVIEW2_BROWSER_ARGUMENTS); + let _window = window_builder .build() .map_err(|err| boxed_error(format!("failed to create app window: {err}")))?; @@ -7898,6 +7911,10 @@ mod tests { #[test] fn settings_preflight_accepts_only_the_fixed_credential_safe_contract() { + assert_eq!( + settings_preflight_arguments("settings_preflight.py", "config.json"), + ["-I", "-B", "settings_preflight.py", "config.json"] + ); assert_eq!( classify_settings_preflight_output(&ProbeCommandOutput { success: true, @@ -8942,6 +8959,16 @@ mod tests { ); } + #[cfg(target_os = "windows")] + #[test] + fn webview_bypasses_the_system_proxy_for_the_private_desktop_api() { + assert!( + WEBVIEW2_BROWSER_ARGUMENTS.contains(&format!("--proxy-bypass-list={DESKTOP_API_HOST}")) + ); + assert!(WEBVIEW2_BROWSER_ARGUMENTS + .contains("--disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection")); + } + #[test] fn generated_session_tokens_are_strongly_sized_and_distinct() { let first = generate_session_token().expect("first token should generate"); diff --git a/web/src/components/settings-panel.test.tsx b/web/src/components/settings-panel.test.tsx index a7a0fc5..f6bf8a0 100644 --- a/web/src/components/settings-panel.test.tsx +++ b/web/src/components/settings-panel.test.tsx @@ -7,6 +7,8 @@ import { afterEach, describe, expect, it, vi } from "vitest" import { SettingsPanel } from "./settings-panel" import { api } from "@/lib/api" +type DesktopWindow = Window & { isTauri?: boolean } + const { mockSettings } = vi.hoisted(() => ({ mockSettings: { llm_provider: "deepseek", @@ -328,9 +330,27 @@ vi.stubGlobal("ResizeObserver", class { afterEach(() => { cleanup() + delete (window as DesktopWindow).isTauri }) describe("SettingsPanel", () => { + it("uses the desktop session directly and never asks for an API token", async () => { + ;(window as DesktopWindow).isTauri = true + vi.mocked(api.settings.get).mockRejectedValueOnce(new TypeError("Failed to fetch")) + + render() + + expect(await screen.findByText("本地服务连接失败")).toBeInTheDocument() + expect(screen.getByText("Failed to fetch")).toBeInTheDocument() + expect(screen.queryByText("API Token")).not.toBeInTheDocument() + expect(screen.queryByPlaceholderText("输入服务器 API Token")).not.toBeInTheDocument() + + fireEvent.click(screen.getByRole("button", { name: "重试连接" })) + + expect(await screen.findByRole("button", { name: "Overall" })).toBeInTheDocument() + expect(screen.queryByText("访问控制")).not.toBeInTheDocument() + }) + it("renders six hierarchical tabs", async () => { render() diff --git a/web/src/components/settings-panel.tsx b/web/src/components/settings-panel.tsx index 934c2ab..a70270e 100644 --- a/web/src/components/settings-panel.tsx +++ b/web/src/components/settings-panel.tsx @@ -8,6 +8,7 @@ import { Label } from "@/components/ui/label" import { Separator } from "@/components/ui/separator" import { Switch } from "@/components/ui/switch" import { api, getApiToken, persistApiToken, type Settings, type YtdlpStatus } from "@/lib/api" +import { isTauriRuntime } from "@/lib/tauri" import { usePreferences } from "@/hooks/use-preferences" import { ProxySetting, SettingRow } from "@/components/settings/setting-controls" import { LocalModelSettings, PurposeModelBindings, RegistrySettings } from "@/components/settings/model-sections" @@ -34,6 +35,7 @@ const TABS: TabDef[] = [ ] export function SettingsPanel() { + const desktopRuntime = isTauriRuntime() const [settings, setSettings] = useState(null) const [saving, setSaving] = useState>({}) const [saved, setSaved] = useState>({}) @@ -105,6 +107,12 @@ export function SettingsPanel() { setAuthenticating(false) } + const retryDesktopSettings = async () => { + setAuthenticating(true) + await loadSettings() + setAuthenticating(false) + } + const updateSettings = useCallback( async (updates: Record) => { const keys = Object.keys(updates) @@ -144,6 +152,42 @@ export function SettingsPanel() { } if (!settings) { + if (desktopRuntime) { + return ( +
+ + + + {loadError ? "本地服务连接失败" : "正在连接本地服务"} + + + + {loadError ? ( + <> +

+ {loadError} +

+ + + ) : ( +
+ + 正在读取桌面设置… +
+ )} +
+
+
+ ) + } + return (
@@ -494,24 +538,26 @@ export function SettingsPanel() { - {/* Security */} - - - 访问控制 - - - - - + {/* Browser/server access control is configured outside the desktop shell. */} + {!desktopRuntime && ( + + + 访问控制 + + + + + + )} {/* Queue */}