From 53546c11aadeb7d7b6cd1e90e45e501a2f6637f1 Mon Sep 17 00:00:00 2001 From: HarenDev Date: Tue, 10 Mar 2026 00:31:58 -0400 Subject: [PATCH 01/30] Pathing issues fix + extract_video_frame util using opencv2 so as to not rely on sam2's own implementation --- backend/api.py | 108 ++++++++++++++++-- backend/utils.py | 58 ++++++++++ .../src/app/services/backend.service.ts | 6 +- .../video-masker/video-masker.component.html | 4 +- .../video-masker/video-masker.component.ts | 17 ++- 5 files changed, 174 insertions(+), 19 deletions(-) diff --git a/backend/api.py b/backend/api.py index c4f1c9a..9ef1017 100644 --- a/backend/api.py +++ b/backend/api.py @@ -1,5 +1,5 @@ from typing import Optional -from fastapi import FastAPI +from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel import numpy as np @@ -31,6 +31,48 @@ video_frame_files: list[str] = [] tracking_video: Optional[np.ndarray] = None tracking_video_path: Optional[str] = None +PROJECT_ROOT = Path(__file__).resolve().parent.parent +IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".bmp"} +VIDEO_EXTENSIONS = {".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v"} +GENERATED_FRAMES_ROOT = PROJECT_ROOT / ".data_engine_frames" + + +def _normalize_input_path(path_value: str) -> str: + return path_value.strip().strip('"').strip("'") + + +def _resolve_input_path(path_value: str, expect_dir: Optional[bool] = None) -> Path: + normalized = _normalize_input_path(path_value) + if not normalized: + raise HTTPException(status_code=400, detail="Path cannot be empty.") + + expanded = Path(os.path.expandvars(os.path.expanduser(normalized))) + + if expanded.is_absolute(): + candidate_paths = [expanded.resolve()] + else: + cwd_candidate = (Path.cwd() / expanded).resolve() + project_candidate = (PROJECT_ROOT / expanded).resolve() + candidate_paths = [cwd_candidate] + if project_candidate != cwd_candidate: + candidate_paths.append(project_candidate) + + resolved_path = next((path for path in candidate_paths if path.exists()), candidate_paths[0]) + + if not resolved_path.exists(): + tried_paths = ", ".join(str(path) for path in candidate_paths) + raise HTTPException( + status_code=404, + detail=f"Path not found: '{normalized}'. Tried: {tried_paths}" + ) + + if expect_dir is True and not resolved_path.is_dir(): + raise HTTPException(status_code=400, detail=f"Expected a directory path, got file: {resolved_path}") + + if expect_dir is False and not resolved_path.is_file(): + raise HTTPException(status_code=400, detail=f"Expected a file path, got directory: {resolved_path}") + + return resolved_path class VideoInitStateRequest(BaseModel): @@ -108,19 +150,59 @@ async def init_video_state(request: VideoInitStateRequest): if video_masker is None: video_masker = svm.SAM2VideoMasker() - video_dir = request.video_frames_dir + resolved_input_path = _resolve_input_path(request.video_frames_dir) + + source_video_path = None + if resolved_input_path.is_file(): + suffix = resolved_input_path.suffix.lower() + if suffix in VIDEO_EXTENSIONS: + try: + resolved_video_dir = extract_video_to_frames( + resolved_input_path, + output_root=GENERATED_FRAMES_ROOT, + image_extensions=IMAGE_EXTENSIONS, + ) + except ValueError as error: + raise HTTPException(status_code=400, detail=str(error)) from error + except Exception as error: + raise HTTPException(status_code=500, detail=str(error)) from error + source_video_path = str(resolved_input_path) + else: + if suffix in IMAGE_EXTENSIONS: + detail = ( + f"Expected a frames directory or video file, got a single image file: {resolved_input_path}. " + "Provide a directory containing image frames." + ) + else: + detail = ( + f"Unsupported input file type: {resolved_input_path.suffix or ''}. " + "Provide a directory of image frames or a video file (.mp4, .mov, .avi, .mkv, .webm, .m4v)." + ) + raise HTTPException(status_code=400, detail=detail) + else: + resolved_video_dir = resolved_input_path + + video_dir = str(resolved_video_dir) video_masker.init_state(video_dir) # Scan for image files - valid_extensions = {".jpg", ".jpeg", ".png", ".bmp"} video_frame_files = sorted([ - f for f in os.listdir(video_dir) - if os.path.splitext(f)[1].lower() in valid_extensions + frame_path.name + for frame_path in resolved_video_dir.iterdir() + if frame_path.is_file() and frame_path.suffix.lower() in IMAGE_EXTENSIONS ]) + + if not video_frame_files: + raise HTTPException( + status_code=400, + detail=f"No image frames found in directory: {resolved_video_dir}" + ) return { "message": "Video state initialized successfully", - "num_frames": len(video_frame_files) + "num_frames": len(video_frame_files), + "resolved_video_frames_dir": video_dir, + "source_video_path": source_video_path } @app.post("/video/reset_state") @@ -232,15 +314,17 @@ async def load_tracking_video(request: TrackingLoadVideoRequest): if tracker is None: tracker = cot.CoTracker() - tracking_video_path = request.video_path + resolved_video_path = _resolve_input_path(request.video_path, expect_dir=False) + tracking_video_path = str(resolved_video_path) # Load video using mediapy - tracking_video = mediapy.read_video(request.video_path) + tracking_video = mediapy.read_video(tracking_video_path) return { "message": "Video loaded successfully", "shape": tracking_video.shape, - "num_frames": tracking_video.shape[0] + "num_frames": tracking_video.shape[0], + "resolved_video_path": tracking_video_path } @@ -360,5 +444,7 @@ async def get_video_frame(frame_idx: int): if frame_idx < 0 or frame_idx >= len(video_frame_files): return {"error": "Frame index out of bounds"} - file_path = os.path.join(video_dir, video_frame_files[frame_idx]) - return FileResponse(file_path) + file_path = Path(video_dir) / video_frame_files[frame_idx] + if not file_path.exists(): + raise HTTPException(status_code=404, detail=f"Frame file not found: {file_path}") + return FileResponse(str(file_path)) diff --git a/backend/utils.py b/backend/utils.py index edd0595..1514710 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -2,6 +2,7 @@ import cv2 from pathlib import Path import shutil +import hashlib def show_mask(image, mask, random_color=False, borders=True): @@ -157,3 +158,60 @@ def save_video_masks(video_dir, video_segments): saved_paths[frame_idx].append(str(output_path)) return saved_paths + + +def extract_video_to_frames(video_path: Path, output_root: Path, image_extensions: set[str] | None = None) -> Path: + """ + Extract a video into a cached frame directory. + + Parameters: + - video_path: path to the source video file + - output_root: root directory where extracted frame folders are stored + - image_extensions: frame extensions considered valid for cache checks + + Returns: Path to directory containing extracted frame images + """ + valid_image_extensions = image_extensions or {".jpg", ".jpeg", ".png", ".bmp"} + + file_stats = video_path.stat() + cache_key = hashlib.sha1( + f"{video_path.resolve()}:{file_stats.st_size}:{file_stats.st_mtime_ns}".encode("utf-8") + ).hexdigest()[:12] + + output_dir = output_root / f"{video_path.stem}_{cache_key}" + output_root.mkdir(parents=True, exist_ok=True) + + if output_dir.exists(): + cached_frames = [ + frame_path + for frame_path in output_dir.iterdir() + if frame_path.is_file() and frame_path.suffix.lower() in valid_image_extensions + ] + if cached_frames: + return output_dir + shutil.rmtree(output_dir, ignore_errors=True) + + output_dir.mkdir(parents=True, exist_ok=True) + + capture = cv2.VideoCapture(str(video_path)) + if not capture.isOpened(): + raise ValueError(f"Unable to open video file: {video_path}") + + frame_idx = 0 + try: + while True: + success, frame = capture.read() + if not success: + break + output_path = output_dir / f"{frame_idx:05d}.jpg" + if not cv2.imwrite(str(output_path), frame): + raise RuntimeError(f"Failed to write extracted frame: {output_path}") + frame_idx += 1 + finally: + capture.release() + + if frame_idx == 0: + shutil.rmtree(output_dir, ignore_errors=True) + raise ValueError(f"No frames could be extracted from video: {video_path}") + + return output_dir diff --git a/frontend-ng/data-engine/src/app/services/backend.service.ts b/frontend-ng/data-engine/src/app/services/backend.service.ts index 30ea142..eaa9249 100644 --- a/frontend-ng/data-engine/src/app/services/backend.service.ts +++ b/frontend-ng/data-engine/src/app/services/backend.service.ts @@ -45,8 +45,12 @@ export class BackendService { constructor(private http: HttpClient) { } + private normalizePath(path: string): string { + return path.trim().replace(/^['\"]|['\"]$/g, ''); + } + initVideoState(dir: string): Observable { - return this.http.post(`${this.apiUrl}/video/init_state`, { video_frames_dir: dir }); + return this.http.post(`${this.apiUrl}/video/init_state`, { video_frames_dir: this.normalizePath(dir) }); } resetVideoState(): Observable { diff --git a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.html b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.html index c83ad15..8e68c33 100644 --- a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.html +++ b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.html @@ -1,8 +1,8 @@
- - +
diff --git a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.ts b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.ts index 38c4113..0f63187 100644 --- a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.ts +++ b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.ts @@ -1,4 +1,4 @@ -import { Component, ElementRef, ViewChild, signal, effect, computed } from '@angular/core'; +import { Component, ElementRef, ViewChild, signal, effect } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { firstValueFrom } from 'rxjs'; @@ -55,18 +55,25 @@ export class VideoMaskerComponent { } async initVideo() { - if (!this.videoDir()) return; + const enteredPath = this.videoDir().trim().replace(/^['\"]|['\"]$/g, ''); + if (!enteredPath) { + alert('Please enter a valid video frames directory path.'); + return; + } + + this.videoDir.set(enteredPath); this.isLoading.set(true); try { - const res = await firstValueFrom(this.backend.initVideoState(this.videoDir())); + const res = await firstValueFrom(this.backend.initVideoState(enteredPath)); this.numFrames.set(res.num_frames); this.isInitialized.set(true); this.currentFrameIdx.set(0); this.objects.set([{ id: 1, name: 'Object 1', color: this.getRandomColor() }]); this.selectedObjectId.set(1); - } catch (err) { + } catch (err: any) { console.error(err); - alert('Failed to initialize video'); + const errorMessage = err?.error?.detail || err?.error?.error || 'Failed to initialize video'; + alert(errorMessage); } finally { this.isLoading.set(false); } From b47237d486fc4c0520108b89d78dffa380528c3b Mon Sep 17 00:00:00 2001 From: HarenDev Date: Tue, 10 Mar 2026 00:42:07 -0400 Subject: [PATCH 02/30] Changed extraction path --- .gitignore | 3 ++- backend/api.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index b39abd6..8484239 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ video/ bedroom/ build/ apple_* -*.log \ No newline at end of file +*.log +/backend/.data_engine_frames/* \ No newline at end of file diff --git a/backend/api.py b/backend/api.py index 9ef1017..771e8d8 100644 --- a/backend/api.py +++ b/backend/api.py @@ -34,7 +34,7 @@ PROJECT_ROOT = Path(__file__).resolve().parent.parent IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".bmp"} VIDEO_EXTENSIONS = {".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v"} -GENERATED_FRAMES_ROOT = PROJECT_ROOT / ".data_engine_frames" +GENERATED_FRAMES_ROOT = PROJECT_ROOT / "backend/.data_engine_frames" def _normalize_input_path(path_value: str) -> str: From e43078c2b17f34a8d48b3aa36bcf0d1446d0308a Mon Sep 17 00:00:00 2001 From: HarenDev Date: Tue, 10 Mar 2026 16:11:09 -0400 Subject: [PATCH 03/30] Path validation fixes --- backend/api.py | 19 +++++++- .../src/app/services/backend.service.ts | 47 ++++++++++++++++++- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/backend/api.py b/backend/api.py index 771e8d8..9fac565 100644 --- a/backend/api.py +++ b/backend/api.py @@ -11,6 +11,7 @@ import mediapy from fastapi.responses import FileResponse import os +from urllib.parse import unquote, urlparse app = FastAPI() @@ -38,7 +39,23 @@ def _normalize_input_path(path_value: str) -> str: - return path_value.strip().strip('"').strip("'") + normalized = str(path_value).strip().strip('"').strip("'") + if not normalized: + return normalized + + if normalized.startswith("file://"): + parsed = urlparse(normalized) + if parsed.scheme == "file": + normalized = parsed.path or "" + if parsed.netloc and parsed.netloc != "localhost": + normalized = f"//{parsed.netloc}{normalized}" + + if os.name == "nt" and normalized.startswith("/") and len(normalized) > 2 and normalized[2] == ":": + normalized = normalized[1:] + + normalized = unquote(normalized) + normalized = normalized.replace("\\ ", " ") + return normalized def _resolve_input_path(path_value: str, expect_dir: Optional[bool] = None) -> Path: diff --git a/frontend-ng/data-engine/src/app/services/backend.service.ts b/frontend-ng/data-engine/src/app/services/backend.service.ts index eaa9249..18b4a29 100644 --- a/frontend-ng/data-engine/src/app/services/backend.service.ts +++ b/frontend-ng/data-engine/src/app/services/backend.service.ts @@ -41,12 +41,55 @@ export interface VideoPropagateResponse { providedIn: 'root' }) export class BackendService { - private apiUrl = 'http://localhost:8000'; + private readonly apiUrl = this.resolveApiUrl(); constructor(private http: HttpClient) { } + private resolveApiUrl(): string { + const globalConfig = (globalThis as { __DATA_ENGINE_API_URL__?: string }).__DATA_ENGINE_API_URL__; + const localStorageConfig = typeof localStorage !== 'undefined' + ? localStorage.getItem('dataEngineApiUrl') + : null; + + const browserHost = typeof window !== 'undefined' && window.location.hostname + ? window.location.hostname + : '127.0.0.1'; + const fallback = `http://${browserHost}:8000`; + return (globalConfig || localStorageConfig || fallback).replace(/\/+$/, ''); + } + + private safeDecodeURIComponent(value: string): string { + try { + return decodeURIComponent(value); + } catch { + return value; + } + } + private normalizePath(path: string): string { - return path.trim().replace(/^['\"]|['\"]$/g, ''); + let normalized = path.trim().replace(/^['\"]|['\"]$/g, ''); + if (!normalized) { + return normalized; + } + + if (normalized.startsWith('file://')) { + try { + const parsed = new URL(normalized); + normalized = parsed.pathname || ''; + if (parsed.host && parsed.host !== 'localhost') { + normalized = `//${parsed.host}${normalized}`; + } + if (/^\/[A-Za-z]:\//.test(normalized)) { + normalized = normalized.slice(1); + } + } catch { + // keep the original input if URL parsing fails + } + } + + normalized = this.safeDecodeURIComponent(normalized); + normalized = normalized.replace(/\\ /g, ' '); + return normalized; } initVideoState(dir: string): Observable { From 1a05b2851e9288d4433e5fa9936103b0395b211b Mon Sep 17 00:00:00 2001 From: HarenDev Date: Tue, 10 Mar 2026 20:01:30 -0400 Subject: [PATCH 04/30] Video resizing occurs on CPU now to avoid big memory pressure on GPU --- backend/co_tracker.py | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/backend/co_tracker.py b/backend/co_tracker.py index 092a466..83ad44b 100644 --- a/backend/co_tracker.py +++ b/backend/co_tracker.py @@ -164,7 +164,7 @@ def paint_point_track( class CoTracker: def __init__(self, model_name="cotracker3_online"): self.device = "cuda" if torch.cuda.is_available() else "cpu" - self.dtype = torch.float32 if self.device == "cuda" else torch.float32 + self.dtype = torch.float32 self.model = torch.hub.load("facebookresearch/co-tracker", model_name) self.model = self.model.to(self.device) @@ -186,12 +186,18 @@ def track(self, video: np.ndarray, queries: Optional[np.ndarray] = None, grid_si - visibility (np.ndarray): The predicted visibility of each point of shape (T, N). """ - # Preprocess video - video_torch = torch.from_numpy(video).permute(0, 3, 1, 2)[None].to(self.device, dtype=self.dtype) # B, T, C, H, W - - # Resize for model input - video_torch_resized = torch.nn.functional.interpolate(video_torch[0], size=VIDEO_INPUT_RESO, mode='bilinear', align_corners=False) - video_torch_resized = video_torch_resized[None] + # Preprocess video on CPU first to avoid holding full-resolution frames on GPU. + # Shape: B, T, C, H, W + video_torch = torch.from_numpy(video).permute(0, 3, 1, 2)[None].float() + + # Resize for model input on CPU, then move the smaller tensor to GPU. + video_torch_resized = torch.nn.functional.interpolate( + video_torch[0], + size=VIDEO_INPUT_RESO, + mode='bilinear', + align_corners=False, + ) + video_torch_resized = video_torch_resized[None].to(self.device, dtype=self.dtype) if queries is None: # Grid tracking @@ -218,14 +224,15 @@ def track(self, video: np.ndarray, queries: Optional[np.ndarray] = None, grid_si # For online models, we need to initialize the video processing first actual_model.init_video_online_processing() - pred_tracks, pred_visibility = actual_model( - video=video_torch_resized, - queries=queries_torch, - iters=4, - is_train=False, - add_space_attn=add_support_grid, - is_online=False # We process the whole video at once, not in online mode - )[:2] # Get only tracks and visibility, ignore confidence and train_data + with torch.inference_mode(): + pred_tracks, pred_visibility = actual_model( + video=video_torch_resized, + queries=queries_torch, + iters=4, + is_train=False, + add_space_attn=add_support_grid, + is_online=False # We process the whole video at once, not in online mode + )[:2] # Get only tracks and visibility, ignore confidence and train_data # Scale tracks back to original video resolution H, W = video.shape[1:3] From 305ec9acd0aee6d6d92af2b9b83a11031d1c23b2 Mon Sep 17 00:00:00 2001 From: HarenDev Date: Tue, 10 Mar 2026 20:32:15 -0400 Subject: [PATCH 05/30] "Online Mode" Batching --- backend/api.py | 221 +++++++++++++-- backend/sam2_video_masker.py | 265 +++++++++++++++++- backend/tests/tester.py | 153 ++++++++-- .../src/app/services/backend.service.ts | 31 +- .../video-masker/video-masker.component.ts | 36 ++- 5 files changed, 638 insertions(+), 68 deletions(-) diff --git a/backend/api.py b/backend/api.py index 9fac565..912b1fe 100644 --- a/backend/api.py +++ b/backend/api.py @@ -3,6 +3,8 @@ from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel import numpy as np +import gc +import logging import co_tracker as cot import sam2_video_masker as svm from utils import * @@ -11,6 +13,7 @@ import mediapy from fastapi.responses import FileResponse import os +import torch from urllib.parse import unquote, urlparse app = FastAPI() @@ -36,6 +39,16 @@ IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".bmp"} VIDEO_EXTENSIONS = {".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v"} GENERATED_FRAMES_ROOT = PROJECT_ROOT / "backend/.data_engine_frames" +DEFAULT_MAX_MASK_FRAMES_IN_RESPONSE = int(os.getenv("VIDEO_PROPAGATE_MAX_MASK_FRAMES", "0")) +DEFAULT_MAX_MASK_VALUES_IN_RESPONSE = int(os.getenv("VIDEO_PROPAGATE_MAX_MASK_VALUES", "0")) + +logger = logging.getLogger(__name__) + + +def _cleanup_cuda_memory(): + gc.collect() + if torch.cuda.is_available(): + torch.cuda.empty_cache() def _normalize_input_path(path_value: str) -> str: @@ -92,8 +105,49 @@ def _resolve_input_path(path_value: str, expect_dir: Optional[bool] = None) -> P return resolved_path +def _serialize_video_segments_for_response( + video_segments: dict, + *, + max_frames: int, + max_mask_values: int, +) -> tuple[dict, bool, int, int]: + """Serialize masks to JSON-safe payload with optional size limits.""" + serialized: dict[int, dict[int, list]] = {} + total_mask_values = 0 + returned_frames = 0 + + for frame_idx, obj_dict in sorted(video_segments.items(), key=lambda item: int(item[0])): + if max_frames >= 0 and returned_frames >= max_frames: + break + + frame_masks: dict[int, np.ndarray] = {} + frame_mask_values = 0 + for obj_id, mask in obj_dict.items(): + mask_array = np.asarray(mask) + frame_mask_values += int(mask_array.size) + frame_masks[int(obj_id)] = mask_array + + if max_mask_values >= 0 and (total_mask_values + frame_mask_values) > max_mask_values: + break + + serialized[int(frame_idx)] = { + obj_id: mask_array.tolist() + for obj_id, mask_array in frame_masks.items() + } + total_mask_values += frame_mask_values + returned_frames += 1 + + truncated = returned_frames < len(video_segments) + return serialized, truncated, returned_frames, total_mask_values + + class VideoInitStateRequest(BaseModel): video_frames_dir: str + online_mode: bool = True + batch_size: Optional[int] = None + offload_video_to_cpu: Optional[bool] = None + offload_state_to_cpu: Optional[bool] = None + async_loading_frames: bool = False class VideoAddPointsOrBoxRequest(BaseModel): frame_idx: int @@ -107,6 +161,11 @@ class VideoPropagateRequest(BaseModel): start_frame_idx: Optional[int] = None max_frame_num_to_track: Optional[int] = None reverse: bool = False + batch_size: Optional[int] = None + online_mode: Optional[bool] = None + include_masks_in_response: bool = False + max_frames_in_response: Optional[int] = None + max_mask_values_in_response: Optional[int] = None class VideoAddMaskRequest(BaseModel): @@ -162,6 +221,7 @@ async def init_video_state(request: VideoInitStateRequest): tracker = None tracking_video = None tracking_video_path = None + _cleanup_cuda_memory() # Initialize video masker if not already created if video_masker is None: @@ -200,7 +260,17 @@ async def init_video_state(request: VideoInitStateRequest): resolved_video_dir = resolved_input_path video_dir = str(resolved_video_dir) - video_masker.init_state(video_dir) + try: + video_masker.init_state( + video_dir, + online_mode=request.online_mode, + batch_size=request.batch_size, + offload_video_to_cpu=request.offload_video_to_cpu, + offload_state_to_cpu=request.offload_state_to_cpu, + async_loading_frames=request.async_loading_frames, + ) + except ValueError as error: + raise HTTPException(status_code=400, detail=str(error)) from error # Scan for image files video_frame_files = sorted([ @@ -219,7 +289,11 @@ async def init_video_state(request: VideoInitStateRequest): "message": "Video state initialized successfully", "num_frames": len(video_frame_files), "resolved_video_frames_dir": video_dir, - "source_video_path": source_video_path + "source_video_path": source_video_path, + "online_mode": video_masker.online_mode, + "batch_size": video_masker.default_batch_size, + "offload_video_to_cpu": video_masker.offload_video_to_cpu, + "offload_state_to_cpu": video_masker.offload_state_to_cpu, } @app.post("/video/reset_state") @@ -279,24 +353,65 @@ async def propagate_in_video(request: VideoPropagateRequest): if video_dir is None: return {"error": "Video directory not set. Call /video/init_state first."} - video_segments = video_masker.propagate_in_video( - start_frame_idx=request.start_frame_idx, - max_frame_num_to_track=request.max_frame_num_to_track, - reverse=request.reverse - ) - - # Save masks for each frame - saved_mask_paths = save_video_masks(video_dir, video_segments) + try: + video_segments = video_masker.propagate_in_video( + start_frame_idx=request.start_frame_idx, + max_frame_num_to_track=request.max_frame_num_to_track, + reverse=request.reverse, + batch_size=request.batch_size, + online_mode=request.online_mode, + ) + except ValueError as error: + raise HTTPException(status_code=400, detail=str(error)) from error - # Convert masks to lists for JSON serialization - video_segments_serializable = {} - for frame_idx, obj_dict in video_segments.items(): - video_segments_serializable[frame_idx] = { - obj_id: mask.tolist() for obj_id, mask in obj_dict.items() - } + try: + saved_mask_paths = save_video_masks(video_dir, video_segments) + except Exception as error: + raise HTTPException(status_code=500, detail=f"Failed to save propagated masks: {error}") from error + + saved_mask_paths_serializable = { + int(frame_idx): [str(path) for path in paths] + for frame_idx, paths in saved_mask_paths.items() + } + + max_frames_in_response = request.max_frames_in_response + if max_frames_in_response is None: + max_frames_in_response = DEFAULT_MAX_MASK_FRAMES_IN_RESPONSE + + max_mask_values_in_response = request.max_mask_values_in_response + if max_mask_values_in_response is None: + max_mask_values_in_response = DEFAULT_MAX_MASK_VALUES_IN_RESPONSE + + video_segments_serializable: dict[int, dict[int, list]] = {} + video_segments_truncated = False + video_segments_returned_frames = 0 + video_segments_returned_mask_values = 0 + + if request.include_masks_in_response: + try: + video_segments_serializable, video_segments_truncated, video_segments_returned_frames, video_segments_returned_mask_values = _serialize_video_segments_for_response( + video_segments, + max_frames=max_frames_in_response, + max_mask_values=max_mask_values_in_response, + ) + except MemoryError: + logger.warning("Mask serialization skipped due to memory pressure.") + video_segments_serializable = {} + video_segments_truncated = len(video_segments) > 0 + except Exception: + logger.exception("Mask serialization failed; returning saved mask paths only.") + video_segments_serializable = {} + video_segments_truncated = len(video_segments) > 0 + return { "video_segments": video_segments_serializable, - "saved_mask_paths": saved_mask_paths + "video_segments_total_frames": len(video_segments), + "video_segments_returned_frames": video_segments_returned_frames, + "video_segments_returned_mask_values": video_segments_returned_mask_values, + "video_segments_truncated": video_segments_truncated, + "saved_mask_paths": saved_mask_paths_serializable, + "online_mode": video_masker.online_mode if request.online_mode is None else bool(request.online_mode), + "batch_size": video_masker.default_batch_size if request.batch_size is None else int(request.batch_size), } @app.post("/video/clear_all_prompts_in_frame") @@ -326,6 +441,7 @@ async def load_tracking_video(request: TrackingLoadVideoRequest): del video_masker video_masker = None video_dir = None + _cleanup_cuda_memory() # Initialize tracker if not already created if tracker is None: @@ -357,12 +473,27 @@ async def track_grid(request: TrackingGridRequest): return {"error": "No video loaded. Call /tracking/load_video first."} # Run tracking - tracks, visibility = tracker.track( - tracking_video, - queries=None, - grid_size=request.grid_size, - add_support_grid=request.add_support_grid - ) + try: + tracks, visibility = tracker.track( + tracking_video, + queries=None, + grid_size=request.grid_size, + add_support_grid=request.add_support_grid + ) + except torch.OutOfMemoryError as error: + _cleanup_cuda_memory() + raise HTTPException( + status_code=507, + detail="CUDA out of memory during tracking. Try a smaller tracking workload and rerun.", + ) from error + except RuntimeError as error: + if "out of memory" in str(error).lower(): + _cleanup_cuda_memory() + raise HTTPException( + status_code=507, + detail="CUDA out of memory during tracking. Try a smaller tracking workload and rerun.", + ) from error + raise # Save visualization painted_video = cot.paint_point_track(tracking_video, tracks, visibility) @@ -383,6 +514,8 @@ async def track_grid(request: TrackingGridRequest): pass mediapy.write_video(str(output_path), painted_video, fps=fps) + + _cleanup_cuda_memory() return { "message": "Grid tracking completed", @@ -409,11 +542,26 @@ async def track_points(request: TrackingPointsRequest): queries = np.array(request.queries) # Run tracking - tracks, visibility = tracker.track( - tracking_video, - queries=queries, - add_support_grid=request.add_support_grid - ) + try: + tracks, visibility = tracker.track( + tracking_video, + queries=queries, + add_support_grid=request.add_support_grid + ) + except torch.OutOfMemoryError as error: + _cleanup_cuda_memory() + raise HTTPException( + status_code=507, + detail="CUDA out of memory during tracking. Try a smaller tracking workload and rerun.", + ) from error + except RuntimeError as error: + if "out of memory" in str(error).lower(): + _cleanup_cuda_memory() + raise HTTPException( + status_code=507, + detail="CUDA out of memory during tracking. Try a smaller tracking workload and rerun.", + ) from error + raise # Save visualization painted_video = cot.paint_point_track(tracking_video, tracks, visibility) @@ -434,6 +582,8 @@ async def track_points(request: TrackingPointsRequest): pass mediapy.write_video(str(output_path), painted_video, fps=fps) + + _cleanup_cuda_memory() return { "message": "Point tracking completed", @@ -465,3 +615,18 @@ async def get_video_frame(frame_idx: int): if not file_path.exists(): raise HTTPException(status_code=404, detail=f"Frame file not found: {file_path}") return FileResponse(str(file_path)) + + +@app.get("/video/mask_frame/{frame_idx}") +async def get_video_mask_frame(frame_idx: int): + global video_dir + if video_dir is None: + return {"error": "Video not initialized"} + + if frame_idx < 0: + return {"error": "Frame index out of bounds"} + + file_path = Path(video_dir) / "masks" / f"frame_{frame_idx:05d}_masks.png" + if not file_path.exists(): + raise HTTPException(status_code=404, detail=f"Mask frame not found: {file_path}") + return FileResponse(str(file_path)) diff --git a/backend/sam2_video_masker.py b/backend/sam2_video_masker.py index eedd78c..e204443 100644 --- a/backend/sam2_video_masker.py +++ b/backend/sam2_video_masker.py @@ -1,12 +1,20 @@ import torch import numpy as np import os +from pathlib import Path from sam2.sam2_video_predictor import SAM2VideoPredictor +from utils import extract_video_to_frames # if using Apple MPS, fall back to CPU for unsupported ops os.environ["PYTORCH_ENABLE_MPS_FALLBACK"] = "1" +IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".bmp"} +VIDEO_EXTENSIONS = {".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v"} +PROJECT_ROOT = Path(__file__).resolve().parent.parent +GENERATED_FRAMES_ROOT = PROJECT_ROOT / "backend/.data_engine_frames" + + class SAM2VideoMasker: def __init__(self): if torch.cuda.is_available(): @@ -33,9 +41,70 @@ def __init__(self): self.predictor = SAM2VideoPredictor.from_pretrained("facebook/sam2-hiera-large") self.inference_state = None + self.online_mode = True + self.default_batch_size = 32 + self.offload_video_to_cpu = True + self.offload_state_to_cpu = False - def init_state(self, video_dir): - self.inference_state = self.predictor.init_state(video_path=video_dir) + def init_state( + self, + video_dir, + online_mode=True, + batch_size=None, + offload_video_to_cpu=None, + offload_state_to_cpu=None, + async_loading_frames=False, + ): + self.online_mode = bool(online_mode) + + if batch_size is not None: + batch_size = int(batch_size) + if batch_size <= 0: + raise ValueError("batch_size must be a positive integer.") + self.default_batch_size = batch_size + + if offload_video_to_cpu is None: + offload_video_to_cpu = self.online_mode + if offload_state_to_cpu is None: + offload_state_to_cpu = False + + self.offload_video_to_cpu = bool(offload_video_to_cpu) + self.offload_state_to_cpu = bool(offload_state_to_cpu) + + resolved_input_path = Path(video_dir).expanduser() + if not resolved_input_path.is_absolute(): + resolved_input_path = (Path.cwd() / resolved_input_path).resolve() + else: + resolved_input_path = resolved_input_path.resolve() + + if not resolved_input_path.exists(): + raise ValueError(f"Path not found: {resolved_input_path}") + + if resolved_input_path.is_file(): + suffix = resolved_input_path.suffix.lower() + if suffix in VIDEO_EXTENSIONS: + resolved_video_dir = extract_video_to_frames( + resolved_input_path, + output_root=GENERATED_FRAMES_ROOT, + image_extensions=IMAGE_EXTENSIONS, + ) + elif suffix in IMAGE_EXTENSIONS: + raise ValueError( + f"Expected a frames directory or video file, got image file: {resolved_input_path}." + ) + else: + raise ValueError( + f"Unsupported input file type: {resolved_input_path.suffix or ''}." + ) + else: + resolved_video_dir = resolved_input_path + + self.inference_state = self.predictor.init_state( + video_path=str(resolved_video_dir), + offload_video_to_cpu=self.offload_video_to_cpu, + offload_state_to_cpu=self.offload_state_to_cpu, + async_loading_frames=async_loading_frames, + ) self.predictor.reset_state(self.inference_state) def reset_state(self): @@ -65,9 +134,197 @@ def add_new_mask(self, frame_idx, obj_id, mask): return frame_idx, out_obj_ids, out_mask_logits - def propagate_in_video(self, start_frame_idx=None, max_frame_num_to_track=None, reverse=False): + def _required_non_cond_history(self): + num_maskmem = max(1, int(getattr(self.predictor, "num_maskmem", 1))) + stride = max(1, int(getattr(self.predictor, "memory_temporal_stride_for_eval", 1))) + + if num_maskmem <= 1: + memory_window = 0 + else: + memory_window = max(num_maskmem - 1, (num_maskmem - 2) * stride) + + pointer_window = 0 + if bool(getattr(self.predictor, "use_obj_ptrs_in_encoder", False)): + pointer_window = max(0, int(getattr(self.predictor, "max_obj_ptrs_in_encoder", 0)) - 1) + + return max(memory_window, pointer_window) + + def _purge_non_conditioning_outputs(self, anchor_frame_idx, reverse=False): + if self.inference_state is None: + return + + keep_window = self._required_non_cond_history() + 2 + lower_bound = anchor_frame_idx - keep_window + upper_bound = anchor_frame_idx + keep_window + + output_dict_per_obj = self.inference_state.get("output_dict_per_obj", {}) + for obj_output_dict in output_dict_per_obj.values(): + non_cond_outputs = obj_output_dict.get("non_cond_frame_outputs", {}) + if reverse: + stale_keys = [ + frame_idx + for frame_idx in list(non_cond_outputs.keys()) + if frame_idx < anchor_frame_idx or frame_idx > upper_bound + ] + else: + stale_keys = [ + frame_idx + for frame_idx in list(non_cond_outputs.keys()) + if frame_idx > anchor_frame_idx or frame_idx < lower_bound + ] + for frame_idx in stale_keys: + non_cond_outputs.pop(frame_idx, None) + + if self.device.type == "cuda": + torch.cuda.empty_cache() + + def _resolve_start_frame_idx(self, start_frame_idx): + if start_frame_idx is not None: + return int(start_frame_idx) + + return min( + frame_idx + for obj_output_dict in self.inference_state["output_dict_per_obj"].values() + for frame_idx in obj_output_dict["cond_frame_outputs"] + ) + + def _compute_total_frames_to_process( + self, + start_frame_idx, + max_frame_num_to_track=None, + reverse=False, + ): + num_frames = int(self.inference_state["num_frames"]) + if num_frames <= 0: + return 0 + + if reverse: + clamped_start = min(max(int(start_frame_idx), 0), num_frames - 1) + available = clamped_start + 1 + else: + clamped_start = min(max(int(start_frame_idx), 0), num_frames) + available = num_frames - clamped_start + + if max_frame_num_to_track is None: + return available + + requested = int(max_frame_num_to_track) + if requested <= 0: + return 0 + + return min(available, requested) + + def _propagate_in_video_batched( + self, + start_frame_idx=None, + max_frame_num_to_track=None, + reverse=False, + batch_size=None, + ): + if self.inference_state is None: + return {} + + self.predictor.propagate_in_video_preflight(self.inference_state) + + effective_batch_size = self.default_batch_size if batch_size is None else int(batch_size) + if effective_batch_size <= 0: + raise ValueError("batch_size must be a positive integer.") + + num_frames = int(self.inference_state["num_frames"]) + current_start = self._resolve_start_frame_idx(start_frame_idx) + total_frames_to_process = self._compute_total_frames_to_process( + start_frame_idx=current_start, + max_frame_num_to_track=max_frame_num_to_track, + reverse=reverse, + ) + if total_frames_to_process <= 0: + return {} + + total_remaining = total_frames_to_process + video_segments = {} + global_last_processed_frame_idx = None + + while True: + if total_remaining <= 0: + break + + frames_this_batch = min(effective_batch_size, total_remaining) + + # SAM2's max_frame_num_to_track is inclusive with start_frame_idx. + # To process exactly N frames in a batch, pass N-1 here. + predictor_max_frames = max(0, int(frames_this_batch) - 1) + + processed_in_batch = 0 + last_processed_frame_idx = None + processed_before_batch = total_frames_to_process - total_remaining + + for out_frame_idx, out_obj_ids, out_mask_logits in self.predictor.propagate_in_video( + self.inference_state, + start_frame_idx=current_start, + max_frame_num_to_track=predictor_max_frames, + reverse=reverse, + progress_total=total_frames_to_process, + progress_initial=processed_before_batch, + ): + if out_frame_idx not in video_segments: + processed_in_batch += 1 + video_segments[out_frame_idx] = { + out_obj_id: (out_mask_logits[i] > 0.0).squeeze(0).cpu().numpy() + for i, out_obj_id in enumerate(out_obj_ids) + } + last_processed_frame_idx = out_frame_idx + + if processed_in_batch == 0 or last_processed_frame_idx is None: + break + + if global_last_processed_frame_idx is not None: + if reverse and last_processed_frame_idx >= global_last_processed_frame_idx: + break + if not reverse and last_processed_frame_idx <= global_last_processed_frame_idx: + break + global_last_processed_frame_idx = last_processed_frame_idx + + total_remaining -= processed_in_batch + + self._purge_non_conditioning_outputs(last_processed_frame_idx, reverse=reverse) + + if reverse: + next_start = last_processed_frame_idx - 1 + if next_start < 0: + break + else: + next_start = last_processed_frame_idx + 1 + if next_start >= num_frames: + break + + current_start = next_start + + return video_segments + + def propagate_in_video( + self, + start_frame_idx=None, + max_frame_num_to_track=None, + reverse=False, + batch_size=None, + online_mode=None, + ): + use_online_mode = self.online_mode if online_mode is None else bool(online_mode) + if use_online_mode: + return self._propagate_in_video_batched( + start_frame_idx=start_frame_idx, + max_frame_num_to_track=max_frame_num_to_track, + reverse=reverse, + batch_size=batch_size, + ) + video_segments = {} - for out_frame_idx, out_obj_ids, out_mask_logits in self.predictor.propagate_in_video(self.inference_state, start_frame_idx, max_frame_num_to_track, reverse): + for out_frame_idx, out_obj_ids, out_mask_logits in self.predictor.propagate_in_video( + self.inference_state, + start_frame_idx, + max_frame_num_to_track, + reverse, + ): video_segments[out_frame_idx] = { out_obj_id: (out_mask_logits[i] > 0.0).squeeze(0).cpu().numpy() for i, out_obj_id in enumerate(out_obj_ids) diff --git a/backend/tests/tester.py b/backend/tests/tester.py index d06032b..e4f1d69 100755 --- a/backend/tests/tester.py +++ b/backend/tests/tester.py @@ -1,6 +1,8 @@ import base64 import os +import signal import subprocess +import sys import time from datetime import datetime from pathlib import Path @@ -11,24 +13,59 @@ import numpy as np # Configuration -BASE_URL = "http://127.0.0.1:8000" +BASE_URL = os.getenv("DATA_ENGINE_BASE_URL", "http://127.0.0.1:8000") BEDROOM_ZIP_URL = "https://dl.fbaipublicfiles.com/segment_anything_2/assets/bedroom.zip" -BEDROOM_DIR = "bedroom" -VIDEO_DIR = "bedroom" # Will use bedroom frames for video tests -TRACKING_VIDEO_PATH = "../../apple.mp4" -API_FILE = "../api.py" +SCRIPT_DIR = Path(__file__).resolve().parent +BACKEND_DIR = SCRIPT_DIR.parent +PROJECT_ROOT = BACKEND_DIR.parent + +BEDROOM_DIR = Path( + os.path.expandvars( + os.path.expanduser( + os.getenv("DATA_ENGINE_BEDROOM_DIR", str(PROJECT_ROOT / "bedroom")) + ) + ) +) +VIDEO_DIR = BEDROOM_DIR # Will use bedroom frames for video tests +TRACKING_VIDEO_PATH = Path( + os.path.expandvars( + os.path.expanduser( + os.getenv("DATA_ENGINE_TRACKING_VIDEO", str(PROJECT_ROOT / "apple.mp4")) + ) + ) +) +API_FILE = Path( + os.path.expandvars( + os.path.expanduser( + os.getenv("DATA_ENGINE_API_FILE", str(BACKEND_DIR / "api.py")) + ) + ) +) + + +def _env_bool(name: str, default: bool) -> bool: + value = os.getenv(name) + if value is None: + return default + return value.strip().lower() not in {"0", "false", "no", "off"} + + +ONLINE_MODE = _env_bool("DATA_ENGINE_ONLINE_MODE", True) +ONLINE_BATCH_SIZE = int(os.getenv("DATA_ENGINE_BATCH_SIZE", "32")) +OFFLOAD_VIDEO_TO_CPU = _env_bool("DATA_ENGINE_OFFLOAD_VIDEO_TO_CPU", True) +OFFLOAD_STATE_TO_CPU = _env_bool("DATA_ENGINE_OFFLOAD_STATE_TO_CPU", False) def download_and_extract_bedroom(): """Downloads and extracts the bedroom video frames if not already present.""" - if os.path.exists(BEDROOM_DIR) and os.path.isdir(BEDROOM_DIR): + if BEDROOM_DIR.exists() and BEDROOM_DIR.is_dir(): # Check if directory has files - if os.listdir(BEDROOM_DIR): + if any(BEDROOM_DIR.iterdir()): print(f"Bedroom directory already exists with files. Skipping download.") return True print(f"Downloading bedroom.zip from {BEDROOM_ZIP_URL}...") - zip_path = "bedroom.zip" + zip_path = PROJECT_ROOT / "bedroom.zip" try: # Download the file with progress indication @@ -38,7 +75,7 @@ def download_and_extract_bedroom(): total_size = int(response.headers.get('content-length', 0)) downloaded_size = 0 - with open(zip_path, 'wb') as f: + with zip_path.open('wb') as f: for chunk in response.iter_content(chunk_size=8192): if chunk: f.write(chunk) @@ -52,12 +89,12 @@ def download_and_extract_bedroom(): # Extract the zip file print(f"Extracting {zip_path}...") with zipfile.ZipFile(zip_path, 'r') as zip_ref: - zip_ref.extractall('.') + zip_ref.extractall(PROJECT_ROOT) print(f"Extraction complete!") # Clean up the zip file - os.remove(zip_path) + zip_path.unlink(missing_ok=True) print(f"Cleaned up {zip_path}") return True @@ -65,8 +102,7 @@ def download_and_extract_bedroom(): except Exception as e: print(f"\nError downloading or extracting bedroom.zip: {e}") # Clean up partial downloads - if os.path.exists(zip_path): - os.remove(zip_path) + zip_path.unlink(missing_ok=True) return False @@ -88,7 +124,13 @@ def wait_for_server(url, timeout=30): def init_video_state(video_dir): """Calls the /video/init_state endpoint.""" url = f"{BASE_URL}/video/init_state" - payload = {"video_frames_dir": video_dir} + payload = { + "video_frames_dir": str(video_dir), + "online_mode": ONLINE_MODE, + "batch_size": ONLINE_BATCH_SIZE, + "offload_video_to_cpu": OFFLOAD_VIDEO_TO_CPU, + "offload_state_to_cpu": OFFLOAD_STATE_TO_CPU, + } response = requests.post(url, json=payload) if response.status_code == 200: print(f"Video state initialized successfully for '{video_dir}'.") @@ -138,14 +180,24 @@ def propagate_in_video(start_frame_idx=None, max_frame_num_to_track=None, revers payload = { "start_frame_idx": start_frame_idx, "max_frame_num_to_track": max_frame_num_to_track, - "reverse": reverse + "reverse": reverse, + "online_mode": ONLINE_MODE, + "batch_size": ONLINE_BATCH_SIZE, + "include_masks_in_response": False, } response = requests.post(url, json=payload) if response.status_code == 200: response_data = response.json() - num_frames = len(response_data.get("video_segments", {})) + online_mode = response_data.get("online_mode") + batch_size = response_data.get("batch_size") + if online_mode is not None: + print(f"Online batching: {'enabled' if online_mode else 'disabled'} (batch_size={batch_size})") + num_frames = response_data.get("video_segments_total_frames", len(response_data.get("video_segments", {}))) saved_paths = response_data.get("saved_mask_paths", {}) print(f"Propagation successful! Processed {num_frames} frames.") + returned_frames = response_data.get("video_segments_returned_frames", len(response_data.get("video_segments", {}))) + if returned_frames: + print(f"Returned {returned_frames} frame masks in API response.") print(f"Saved masks for {len(saved_paths)} frames.") for frame_idx, paths in saved_paths.items(): print(f" Frame {frame_idx}: {paths}") @@ -154,6 +206,49 @@ def propagate_in_video(start_frame_idx=None, max_frame_num_to_track=None, revers return response.status_code == 200, response.json() if response.status_code == 200 else None +def stop_server_process(server_process): + """Stops FastAPI server process and its child processes reliably.""" + if server_process is None: + return + + if server_process.poll() is not None: + print("Server already stopped.") + return + + try: + if os.name != "nt": + os.killpg(server_process.pid, signal.SIGTERM) + else: + server_process.terminate() + except ProcessLookupError: + print("Server process not found during shutdown.") + return + + try: + server_process.wait(timeout=10) + print("Server shut down successfully.") + return + except subprocess.TimeoutExpired: + print("Server did not terminate in time, forcing kill.") + except KeyboardInterrupt: + print("Interrupted during shutdown, forcing kill.") + + try: + if os.name != "nt": + os.killpg(server_process.pid, signal.SIGKILL) + else: + server_process.kill() + except ProcessLookupError: + pass + + try: + server_process.wait(timeout=5) + except Exception: + pass + + print("Server killed.") + + def run_video_tests(): """Runs the video masking test suite.""" print("\n" + "=" * 60) @@ -161,7 +256,7 @@ def run_video_tests(): print("=" * 60) # Check if video directory exists - if not os.path.exists(VIDEO_DIR): + if not VIDEO_DIR.exists(): print(f"\nVideo directory '{VIDEO_DIR}' not found. Skipping video tests.") return @@ -229,7 +324,7 @@ def run_video_tests(): def load_tracking_video(video_path): """Calls the /tracking/load_video endpoint.""" url = f"{BASE_URL}/tracking/load_video" - response = requests.post(url, json={"video_path": video_path}) + response = requests.post(url, json={"video_path": str(video_path)}) if response.status_code == 200: response_data = response.json() print(f"Video loaded successfully: {video_path}") @@ -285,7 +380,7 @@ def run_tracking_tests(): print("=" * 60) # Check if tracking video exists - if not os.path.exists(TRACKING_VIDEO_PATH): + if not TRACKING_VIDEO_PATH.exists(): print(f"\nTracking video '{TRACKING_VIDEO_PATH}' not found. Skipping tracking tests.") return @@ -354,9 +449,17 @@ def run_tracking_tests(): if not download_and_extract_bedroom(): print("Failed to download bedroom data. Exiting.") exit(1) + + if not API_FILE.exists(): + print(f"API file not found: {API_FILE}") + exit(1) # Start the FastAPI server as a background process - server_process = subprocess.Popen(["fastapi", "dev", API_FILE]) + server_process = subprocess.Popen( + [sys.executable, "-m", "fastapi", "dev", str(API_FILE)], + cwd=str(BACKEND_DIR), + start_new_session=(os.name != "nt"), + ) print(f"\nStarting FastAPI server with PID: {server_process.pid}...") try: @@ -378,12 +481,4 @@ def run_tracking_tests(): finally: # Stop the server print("\nShutting down the server...") - server_process.terminate() - try: - # Wait for the process to terminate - server_process.wait(timeout=10) - print("Server shut down successfully.") - except subprocess.TimeoutExpired: - print("Server did not terminate in time, killing it.") - server_process.kill() - print("Server killed.") + stop_server_process(server_process) diff --git a/frontend-ng/data-engine/src/app/services/backend.service.ts b/frontend-ng/data-engine/src/app/services/backend.service.ts index 18b4a29..c6b358f 100644 --- a/frontend-ng/data-engine/src/app/services/backend.service.ts +++ b/frontend-ng/data-engine/src/app/services/backend.service.ts @@ -4,6 +4,11 @@ import { Observable } from 'rxjs'; export interface VideoInitStateRequest { video_frames_dir: string; + online_mode?: boolean; + batch_size?: number; + offload_video_to_cpu?: boolean; + offload_state_to_cpu?: boolean; + async_loading_frames?: boolean; } export interface VideoAddPointsOrBoxRequest { @@ -19,6 +24,11 @@ export interface VideoPropagateRequest { start_frame_idx?: number; max_frame_num_to_track?: number; reverse?: boolean; + batch_size?: number; + online_mode?: boolean; + include_masks_in_response?: boolean; + max_frames_in_response?: number; + max_mask_values_in_response?: number; } export interface VideoAddMaskRequest { @@ -34,7 +44,11 @@ export interface VideoAddPointsResponse { export interface VideoPropagateResponse { video_segments: { [frame_idx: string]: { [obj_id: string]: boolean[][] } }; - saved_mask_paths: { [frame_idx: string]: { [obj_id: string]: string } }; + saved_mask_paths: { [frame_idx: string]: string[] }; + video_segments_total_frames?: number; + video_segments_returned_frames?: number; + video_segments_returned_mask_values?: number; + video_segments_truncated?: boolean; } @Injectable({ @@ -92,8 +106,15 @@ export class BackendService { return normalized; } - initVideoState(dir: string): Observable { - return this.http.post(`${this.apiUrl}/video/init_state`, { video_frames_dir: this.normalizePath(dir) }); + initVideoState( + dir: string, + options?: Omit + ): Observable { + const payload: VideoInitStateRequest = { + video_frames_dir: this.normalizePath(dir), + ...options + }; + return this.http.post(`${this.apiUrl}/video/init_state`, payload); } resetVideoState(): Observable { @@ -127,4 +148,8 @@ export class BackendService { getVideoFrameUrl(frameIdx: number): string { return `${this.apiUrl}/video/frame/${frameIdx}`; } + + getVideoMaskFrameUrl(frameIdx: number): string { + return `${this.apiUrl}/video/mask_frame/${frameIdx}`; + } } diff --git a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.ts b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.ts index 0f63187..5d4d64f 100644 --- a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.ts +++ b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.ts @@ -43,6 +43,7 @@ export class VideoMaskerComponent { // frameIdx -> objId -> points points = signal>>(new Map()); + useSavedMaskFrames = signal(false); isLoading = signal(false); @@ -68,6 +69,7 @@ export class VideoMaskerComponent { this.numFrames.set(res.num_frames); this.isInitialized.set(true); this.currentFrameIdx.set(0); + this.useSavedMaskFrames.set(false); this.objects.set([{ id: 1, name: 'Object 1', color: this.getRandomColor() }]); this.selectedObjectId.set(1); } catch (err: any) { @@ -84,12 +86,24 @@ export class VideoMaskerComponent { if (!ctx) return; const img = new Image(); - img.src = this.backend.getVideoFrameUrl(frameIdx); + const frameUrl = this.backend.getVideoFrameUrl(frameIdx); + const maskFrameUrl = this.backend.getVideoMaskFrameUrl(frameIdx); + let triedFallbackToRawFrame = false; + + img.onerror = () => { + if (this.useSavedMaskFrames() && !triedFallbackToRawFrame) { + triedFallbackToRawFrame = true; + img.src = frameUrl; + } + }; + img.onload = () => { this.canvasRef.nativeElement.width = img.width; this.canvasRef.nativeElement.height = img.height; this.draw(img); }; + + img.src = this.useSavedMaskFrames() ? maskFrameUrl : frameUrl; } draw(img: HTMLImageElement) { @@ -282,12 +296,15 @@ export class VideoMaskerComponent { async propagate() { this.isLoading.set(true); try { - const res = await firstValueFrom(this.backend.propagateInVideo({})); + const res = await firstValueFrom(this.backend.propagateInVideo({ + include_masks_in_response: false + })); if (res && res.video_segments) { // Update all masks const currentMasksMap = this.masks(); + const entries = Object.entries(res.video_segments); - for (const [frameIdxStr, objMasks] of Object.entries(res.video_segments)) { + for (const [frameIdxStr, objMasks] of entries) { const frameIdx = parseInt(frameIdxStr); let frameMasksMap = currentMasksMap.get(frameIdx); if (!frameMasksMap) { @@ -300,7 +317,17 @@ export class VideoMaskerComponent { frameMasksMap.set(objId, mask as boolean[][]); } } - this.masks.set(new Map(currentMasksMap)); + + const hasSavedMaskFrames = Object.keys(res.saved_mask_paths || {}).length > 0; + const useSavedFrames = hasSavedMaskFrames && entries.length === 0; + this.useSavedMaskFrames.set(useSavedFrames); + + if (useSavedFrames) { + this.masks.set(new Map()); + } else { + this.masks.set(new Map(currentMasksMap)); + } + this.loadFrame(this.currentFrameIdx()); // Redraw current frame } } catch (err) { @@ -314,6 +341,7 @@ export class VideoMaskerComponent { clearMasks() { // This should probably call reset_state on backend this.backend.resetVideoState().subscribe(() => { + this.useSavedMaskFrames.set(false); this.masks.set(new Map()); this.points.set(new Map()); this.loadFrame(this.currentFrameIdx()); From 918578a893dc1a8e8d7a01b797c9d39613bd690a Mon Sep 17 00:00:00 2001 From: HarenDev Date: Wed, 11 Mar 2026 01:18:45 -0400 Subject: [PATCH 06/30] debloated the init_video endpoint by moving them to their own functions that init_video calls --- backend/api.py | 198 ++++++++++++++++++++++++++++--------------------- 1 file changed, 113 insertions(+), 85 deletions(-) diff --git a/backend/api.py b/backend/api.py index 912b1fe..65ba9d5 100644 --- a/backend/api.py +++ b/backend/api.py @@ -105,6 +105,98 @@ def _resolve_input_path(path_value: str, expect_dir: Optional[bool] = None) -> P return resolved_path +def _prepare_video_masker_for_video_init(): + global video_masker, tracker, tracking_video, tracking_video_path, video_dir + + if tracker is not None: + del tracker + tracker = None + tracking_video = None + tracking_video_path = None + _cleanup_cuda_memory() + + if video_masker is None: + video_masker = svm.SAM2VideoMasker() + + +def _initialize_video_state_from_resolved_input( + resolved_input_path: Path, + *, + online_mode: bool, + batch_size: Optional[int], + offload_video_to_cpu: Optional[bool], + offload_state_to_cpu: Optional[bool], + async_loading_frames: bool, +): + global video_masker, video_dir, video_frame_files + + source_video_path = None + if resolved_input_path.is_file(): + suffix = resolved_input_path.suffix.lower() + if suffix in VIDEO_EXTENSIONS: + try: + resolved_video_dir = extract_video_to_frames( + resolved_input_path, + output_root=GENERATED_FRAMES_ROOT, + image_extensions=IMAGE_EXTENSIONS, + ) + except ValueError as error: + raise HTTPException(status_code=400, detail=str(error)) from error + except Exception as error: + raise HTTPException(status_code=500, detail=str(error)) from error + source_video_path = str(resolved_input_path) + else: + if suffix in IMAGE_EXTENSIONS: + detail = ( + f"Expected a frames directory or video file, got a single image file: {resolved_input_path}. " + "Provide a directory containing image frames." + ) + else: + detail = ( + f"Unsupported input file type: {resolved_input_path.suffix or ''}. " + "Provide a directory of image frames or a video file (.mp4, .mov, .avi, .mkv, .webm, .m4v)." + ) + raise HTTPException(status_code=400, detail=detail) + else: + resolved_video_dir = resolved_input_path + + video_dir = str(resolved_video_dir) + try: + video_masker.init_state( + video_dir, + online_mode=online_mode, + batch_size=batch_size, + offload_video_to_cpu=offload_video_to_cpu, + offload_state_to_cpu=offload_state_to_cpu, + async_loading_frames=async_loading_frames, + ) + except ValueError as error: + raise HTTPException(status_code=400, detail=str(error)) from error + + video_frame_files = sorted([ + frame_path.name + for frame_path in resolved_video_dir.iterdir() + if frame_path.is_file() and frame_path.suffix.lower() in IMAGE_EXTENSIONS + ]) + + if not video_frame_files: + raise HTTPException( + status_code=400, + detail=f"No image frames found in directory: {resolved_video_dir}" + ) + + return { + "message": "Video state initialized successfully", + "num_frames": len(video_frame_files), + "resolved_video_frames_dir": video_dir, + "source_video_path": source_video_path, + "online_mode": video_masker.online_mode, + "batch_size": video_masker.default_batch_size, + "offload_video_to_cpu": video_masker.offload_video_to_cpu, + "offload_state_to_cpu": video_masker.offload_state_to_cpu, + } + + def _serialize_video_segments_for_response( video_segments: dict, *, @@ -176,6 +268,7 @@ class VideoAddMaskRequest(BaseModel): class TrackingLoadVideoRequest(BaseModel): video_path: str + model_name: str = "cotracker3_offline" # "cotracker3_offline" or "cotracker3_online" class TrackingGridRequest(BaseModel): @@ -213,88 +306,16 @@ async def status(): @app.post("/video/init_state") async def init_video_state(request: VideoInitStateRequest): - global video_masker, video_dir, tracker, tracking_video, tracking_video_path, video_frame_files - - # Unload tracker if it's currently loaded - if tracker is not None: - del tracker - tracker = None - tracking_video = None - tracking_video_path = None - _cleanup_cuda_memory() - - # Initialize video masker if not already created - if video_masker is None: - video_masker = svm.SAM2VideoMasker() - + _prepare_video_masker_for_video_init() resolved_input_path = _resolve_input_path(request.video_frames_dir) - - source_video_path = None - if resolved_input_path.is_file(): - suffix = resolved_input_path.suffix.lower() - if suffix in VIDEO_EXTENSIONS: - try: - resolved_video_dir = extract_video_to_frames( - resolved_input_path, - output_root=GENERATED_FRAMES_ROOT, - image_extensions=IMAGE_EXTENSIONS, - ) - except ValueError as error: - raise HTTPException(status_code=400, detail=str(error)) from error - except Exception as error: - raise HTTPException(status_code=500, detail=str(error)) from error - source_video_path = str(resolved_input_path) - else: - if suffix in IMAGE_EXTENSIONS: - detail = ( - f"Expected a frames directory or video file, got a single image file: {resolved_input_path}. " - "Provide a directory containing image frames." - ) - else: - detail = ( - f"Unsupported input file type: {resolved_input_path.suffix or ''}. " - "Provide a directory of image frames or a video file (.mp4, .mov, .avi, .mkv, .webm, .m4v)." - ) - raise HTTPException(status_code=400, detail=detail) - else: - resolved_video_dir = resolved_input_path - - video_dir = str(resolved_video_dir) - try: - video_masker.init_state( - video_dir, - online_mode=request.online_mode, - batch_size=request.batch_size, - offload_video_to_cpu=request.offload_video_to_cpu, - offload_state_to_cpu=request.offload_state_to_cpu, - async_loading_frames=request.async_loading_frames, - ) - except ValueError as error: - raise HTTPException(status_code=400, detail=str(error)) from error - - # Scan for image files - video_frame_files = sorted([ - frame_path.name - for frame_path in resolved_video_dir.iterdir() - if frame_path.is_file() and frame_path.suffix.lower() in IMAGE_EXTENSIONS - ]) - - if not video_frame_files: - raise HTTPException( - status_code=400, - detail=f"No image frames found in directory: {resolved_video_dir}" - ) - - return { - "message": "Video state initialized successfully", - "num_frames": len(video_frame_files), - "resolved_video_frames_dir": video_dir, - "source_video_path": source_video_path, - "online_mode": video_masker.online_mode, - "batch_size": video_masker.default_batch_size, - "offload_video_to_cpu": video_masker.offload_video_to_cpu, - "offload_state_to_cpu": video_masker.offload_state_to_cpu, - } + return _initialize_video_state_from_resolved_input( + resolved_input_path, + online_mode=request.online_mode, + batch_size=request.batch_size, + offload_video_to_cpu=request.offload_video_to_cpu, + offload_state_to_cpu=request.offload_state_to_cpu, + async_loading_frames=request.async_loading_frames, + ) @app.post("/video/reset_state") async def reset_video_state(): @@ -443,9 +464,14 @@ async def load_tracking_video(request: TrackingLoadVideoRequest): video_dir = None _cleanup_cuda_memory() - # Initialize tracker if not already created - if tracker is None: - tracker = cot.CoTracker() + # (Re-)initialise tracker with the requested model variant. + # A new tracker is created if the model_name changed or no tracker exists. + requested_model = getattr(request, "model_name", "cotracker3_offline") + if tracker is None or getattr(tracker, "model_name", None) != requested_model: + if tracker is not None: + del tracker + _cleanup_cuda_memory() + tracker = cot.CoTracker(model_name=requested_model) resolved_video_path = _resolve_input_path(request.video_path, expect_dir=False) tracking_video_path = str(resolved_video_path) @@ -455,6 +481,7 @@ async def load_tracking_video(request: TrackingLoadVideoRequest): return { "message": "Video loaded successfully", + "model_name": tracker.model_name, "shape": tracking_video.shape, "num_frames": tracking_video.shape[0], "resolved_video_path": tracking_video_path @@ -570,7 +597,8 @@ async def track_points(request: TrackingPointsRequest): video_name = Path(tracking_video_path).stem output_dir = Path(tracking_video_path).parent timestamp = int(datetime.now().timestamp()) - output_filename = f"{video_name}_tracked_points_{timestamp}.mp4" + output_tag = "tracked_points_support" if request.add_support_grid else "tracked_points" + output_filename = f"{video_name}_{output_tag}_{timestamp}.mp4" output_path = output_dir / output_filename fps = 30 # Default fps From 7d76a9cca9f729a80b0d2dff915fab1c972e30a2 Mon Sep 17 00:00:00 2001 From: HarenDev Date: Wed, 11 Mar 2026 01:45:18 -0400 Subject: [PATCH 07/30] (more than likely temporary) Trying to fix regressions on co_tracker Added both offline and online mode to test which will be better to keep --- backend/api.py | 6 +- backend/co_tracker.py | 169 +++++++++++++++++++++++----------- backend/tests/tester.py | 195 ++++++++++++++++++++++++++++++---------- 3 files changed, 267 insertions(+), 103 deletions(-) diff --git a/backend/api.py b/backend/api.py index 65ba9d5..a2abc50 100644 --- a/backend/api.py +++ b/backend/api.py @@ -529,7 +529,8 @@ async def track_grid(request: TrackingGridRequest): video_name = Path(tracking_video_path).stem output_dir = Path(tracking_video_path).parent timestamp = int(datetime.now().timestamp()) - output_filename = f"{video_name}_tracked_grid_{timestamp}.mp4" + mode_label = "online" if tracker.is_online else "offline" + output_filename = f"{video_name}_tracked_grid_{mode_label}_{timestamp}.mp4" output_path = output_dir / output_filename fps = 30 # Default fps @@ -597,8 +598,9 @@ async def track_points(request: TrackingPointsRequest): video_name = Path(tracking_video_path).stem output_dir = Path(tracking_video_path).parent timestamp = int(datetime.now().timestamp()) + mode_label = "online" if tracker.is_online else "offline" output_tag = "tracked_points_support" if request.add_support_grid else "tracked_points" - output_filename = f"{video_name}_{output_tag}_{timestamp}.mp4" + output_filename = f"{video_name}_{output_tag}_{mode_label}_{timestamp}.mp4" output_path = output_dir / output_filename fps = 30 # Default fps diff --git a/backend/co_tracker.py b/backend/co_tracker.py index 83ad44b..834e36f 100644 --- a/backend/co_tracker.py +++ b/backend/co_tracker.py @@ -162,13 +162,26 @@ def paint_point_track( return video class CoTracker: - def __init__(self, model_name="cotracker3_online"): + def __init__(self, model_name="cotracker3_offline"): self.device = "cuda" if torch.cuda.is_available() else "cpu" self.dtype = torch.float32 - + self.model_name = model_name + self.is_online = "online" in model_name + + # Load the requested CoTracker model variant: + # • cotracker3_offline — single global attention pass over all frames, + # globally consistent tracks, no sliding-window boundary artefacts. + # • cotracker3_online — sliding window (window_len=16, step=8), lower + # VRAM but may exhibit drift at window boundaries. + # Both are independent of SAM2's online mode (video masking). self.model = torch.hub.load("facebookresearch/co-tracker", model_name) self.model = self.model.to(self.device) + # Increase support grid from default 6×6 (36 pts) to 10×10 (100 pts). + # More support points give the attention mechanism better global context + # for correlation, reducing drift on long sequences and with many points. + self.model.support_grid_size = 10 + def track(self, video: np.ndarray, queries: Optional[np.ndarray] = None, grid_size=15, add_support_grid=True): """ Tracks points in a video. @@ -182,63 +195,111 @@ def track(self, video: np.ndarray, queries: Optional[np.ndarray] = None, grid_si Returns: Tuple[np.ndarray, np.ndarray]: A tuple containing: - - tracks (np.ndarray): The predicted tracks of shape (T, N, 2) for each point. - - visibility (np.ndarray): The predicted visibility of each point of shape (T, N). + - tracks (np.ndarray): The predicted tracks of shape (N, T, 2) for each point. + - visibility (np.ndarray): The predicted visibility of each point of shape (N, T). """ - + # Preprocess video on CPU first to avoid holding full-resolution frames on GPU. - # Shape: B, T, C, H, W - video_torch = torch.from_numpy(video).permute(0, 3, 1, 2)[None].float() - - # Resize for model input on CPU, then move the smaller tensor to GPU. - video_torch_resized = torch.nn.functional.interpolate( - video_torch[0], - size=VIDEO_INPUT_RESO, - mode='bilinear', - align_corners=False, - ) - video_torch_resized = video_torch_resized[None].to(self.device, dtype=self.dtype) + # Shape: (B, T, C, H, W) — the predictor wrapper expects this layout. + video_torch = torch.from_numpy(video).permute(0, 3, 1, 2)[None].float().to(self.device) + + # Build query tensor if user supplied points + queries_torch = None + if queries is not None: + queries = np.asarray(queries, dtype=np.float32) + queries_torch = torch.from_numpy(queries).float()[None].to(self.device) # (1, N, 3) - if queries is None: - # Grid tracking - xy = get_points_on_a_grid(grid_size, video_torch_resized.shape[3:], device=self.device) - queries_torch = torch.cat([torch.zeros_like(xy[:, :, :1]), xy], dim=2).to(self.device) - add_support_grid = False - else: - # Point tracking - # Scale queries to model input resolution - H, W = video.shape[1:3] - queries_scaled = queries.copy() - queries_scaled[:, 1] *= VIDEO_INPUT_RESO[1] / W - queries_scaled[:, 2] *= VIDEO_INPUT_RESO[0] / H - - # Convert to tensor: N, 3 -> 1, N, 3 - queries_torch = torch.tensor(queries_scaled).float()[None].to(self.device, self.dtype) - # tyx -> txy - queries_torch = queries_torch[:, :, [0, 2, 1]] - - # Run tracker using the model's internal forward method - # The torch.hub model is a wrapper (CoTrackerOnlinePredictor), access the actual model - actual_model = self.model.model if hasattr(self.model, 'model') else self.model - - # For online models, we need to initialize the video processing first - actual_model.init_video_online_processing() - with torch.inference_mode(): - pred_tracks, pred_visibility = actual_model( - video=video_torch_resized, + if self.is_online: + pred_tracks, pred_visibility = self._track_online( + video_torch, queries_torch, grid_size, add_support_grid + ) + else: + pred_tracks, pred_visibility = self._track_offline( + video_torch, queries_torch, grid_size, add_support_grid + ) + + # pred_tracks: (B, T, N, 2), pred_visibility: (B, T, N) + # Transpose to (N, T, 2) and (N, T) for our output convention. + tracks_np = pred_tracks[0].permute(1, 0, 2).detach().cpu().numpy() + # Always visible — caller controls rendering. + all_visible = np.ones((tracks_np.shape[0], tracks_np.shape[1]), dtype=bool) + + return tracks_np, all_visible + + # ------------------------------------------------------------------ + # Offline tracking — single global attention pass over all T frames. + # Supports backward_tracking for forward+backward track merging. + # ------------------------------------------------------------------ + def _track_offline(self, video_torch, queries_torch, grid_size, add_support_grid): + if queries_torch is not None: + return self.model( + video_torch, + queries=queries_torch, + add_support_grid=add_support_grid, + backward_tracking=True, + ) + else: + return self.model( + video_torch, + grid_size=grid_size, + grid_query_frame=0, + backward_tracking=True, + ) + + # ------------------------------------------------------------------ + # Online tracking — sliding window (window_len=16, step=8). + # The predictor exposes a chunked `forward` interface: + # 1. First call with is_first_step=True (2×step frames) → (None, None) + # 2. Subsequent calls with step new frames → accumulated tracks. + # ------------------------------------------------------------------ + def _track_online(self, video_torch, queries_torch, grid_size, add_support_grid): + step = self.model.step # 8 for cotracker3_online (window_len=16) + T = video_torch.shape[1] + + if T < 2: + raise ValueError( + f"Video too short ({T} frames) for online tracking " + f"(minimum 2 frames required)." + ) + + # Initialize online state once per video (stores query points internally). + if queries_torch is not None: + self.model( + video_chunk=video_torch, + is_first_step=True, queries=queries_torch, - iters=4, - is_train=False, - add_space_attn=add_support_grid, - is_online=False # We process the whole video at once, not in online mode - )[:2] # Get only tracks and visibility, ignore confidence and train_data - - # Scale tracks back to original video resolution - H, W = video.shape[1:3] - pred_tracks_scaled = pred_tracks * torch.tensor([W, H]).to(self.device) / torch.tensor([VIDEO_INPUT_RESO[1], VIDEO_INPUT_RESO[0]]).to(self.device) - - return pred_tracks_scaled[0].permute(1, 0, 2).detach().cpu().numpy(), pred_visibility[0].permute(1, 0).detach().cpu().numpy() + add_support_grid=add_support_grid, + ) + else: + self.model( + video_chunk=video_torch, + is_first_step=True, + grid_size=grid_size, + grid_query_frame=0, + add_support_grid=False, + ) + + pred_tracks, pred_visibility = None, None + process_add_support_grid = add_support_grid if queries_torch is not None else False + # Match official online API usage from CoTracker: + # for ind in range(0, T - step, step): + # model(video_chunk=video[:, ind : ind + 2*step]) + # For short videos (T <= step), still run a single processing window. + for ind in range(0, max(T - step, 1), step): + pred_tracks, pred_visibility = self.model( + video_chunk=video_torch[:, ind : ind + step * 2], + is_first_step=False, + add_support_grid=process_add_support_grid, + ) + + if pred_tracks is None: + raise ValueError( + f"Online tracking produced no output for {T} frames. " + f"The video may be too short for the sliding window." + ) + + return pred_tracks, pred_visibility if __name__ == '__main__': import argparse diff --git a/backend/tests/tester.py b/backend/tests/tester.py index e4f1d69..d3b119a 100755 --- a/backend/tests/tester.py +++ b/backend/tests/tester.py @@ -321,13 +321,14 @@ def run_video_tests(): reset_video_state() -def load_tracking_video(video_path): +def load_tracking_video(video_path, model_name="cotracker3_offline"): """Calls the /tracking/load_video endpoint.""" url = f"{BASE_URL}/tracking/load_video" - response = requests.post(url, json={"video_path": str(video_path)}) + response = requests.post(url, json={"video_path": str(video_path), "model_name": model_name}) if response.status_code == 200: response_data = response.json() print(f"Video loaded successfully: {video_path}") + print(f" Model: {response_data.get('model_name', 'N/A')}") print(f" Shape: {response_data.get('shape', [])}") print(f" Num frames: {response_data.get('num_frames', 0)}") else: @@ -335,7 +336,51 @@ def load_tracking_video(video_path): return response.status_code == 200, response.json() if response.status_code == 200 else None -def track_grid(grid_size=15, add_support_grid=True): +def _ensure_mode_labeled_output_path(output_video_path: str, expected_mode: str) -> str: + """Ensures the output filename includes the expected mode label. + + If the backend already labels outputs (preferred), this is a no-op. + If the backend returns legacy unlabeled filenames, this renames the file + to include `_offline_` / `_online_` before the timestamp. + """ + if not output_video_path: + return output_video_path + + output_path = Path(output_video_path) + name = output_path.name + opposite_mode = "online" if expected_mode == "offline" else "offline" + + if f"_{expected_mode}_" in name: + return output_video_path + + if f"_{opposite_mode}_" in name: + print( + f"Warning: Output file appears labeled as {opposite_mode}: {output_video_path}. " + f"Leaving filename unchanged." + ) + return output_video_path + + stem = output_path.stem + suffix = output_path.suffix + + # Insert mode before trailing timestamp if present: ..._.mp4 + prefix, sep, maybe_ts = stem.rpartition("_") + if sep and maybe_ts.isdigit() and prefix: + new_stem = f"{prefix}_{expected_mode}_{maybe_ts}" + else: + new_stem = f"{stem}_{expected_mode}" + + new_path = output_path.with_name(f"{new_stem}{suffix}") + + if output_path.exists() and new_path != output_path: + output_path.rename(new_path) + print(f"Renamed output file to include mode label: {new_path}") + return str(new_path) + + return output_video_path + + +def track_grid(grid_size=15, add_support_grid=True, expected_mode=None): """Calls the /tracking/track_grid endpoint.""" url = f"{BASE_URL}/tracking/track_grid" payload = { @@ -345,6 +390,10 @@ def track_grid(grid_size=15, add_support_grid=True): response = requests.post(url, json=payload) if response.status_code == 200: response_data = response.json() + if expected_mode: + response_data["output_video_path"] = _ensure_mode_labeled_output_path( + response_data.get("output_video_path", ""), expected_mode + ) print(f"Grid tracking completed successfully!") print(f" Num points: {response_data.get('num_points', 0)}") print(f" Num frames: {response_data.get('num_frames', 0)}") @@ -354,7 +403,7 @@ def track_grid(grid_size=15, add_support_grid=True): return response.status_code == 200, response.json() if response.status_code == 200 else None -def track_points(queries, add_support_grid=True): +def track_points(queries, add_support_grid=True, expected_mode=None): """Calls the /tracking/track_points endpoint.""" url = f"{BASE_URL}/tracking/track_points" payload = { @@ -364,6 +413,10 @@ def track_points(queries, add_support_grid=True): response = requests.post(url, json=payload) if response.status_code == 200: response_data = response.json() + if expected_mode: + response_data["output_video_path"] = _ensure_mode_labeled_output_path( + response_data.get("output_video_path", ""), expected_mode + ) print(f"Point tracking completed successfully!") print(f" Num points: {response_data.get('num_points', 0)}") print(f" Num frames: {response_data.get('num_frames', 0)}") @@ -373,69 +426,117 @@ def track_points(queries, add_support_grid=True): return response.status_code == 200, response.json() if response.status_code == 200 else None -def run_tracking_tests(): - """Runs the tracking test suite.""" - print("\n" + "=" * 60) - print("RUNNING TRACKING TESTS") - print("=" * 60) - +def _run_tracking_suite(model_name: str): + """Runs the tracking test suite for a specific CoTracker model variant. + + Args: + model_name: "cotracker3_offline" or "cotracker3_online". + """ + label = model_name.upper().replace("COTRACKER3_", "") + mode_key = "online" if "online" in model_name else "offline" + generated_outputs = [] + + print(f"\n{'─' * 50}") + print(f" CoTracker mode: {label} ({model_name})") + print(f"{'─' * 50}") + # Check if tracking video exists if not TRACKING_VIDEO_PATH.exists(): - print(f"\nTracking video '{TRACKING_VIDEO_PATH}' not found. Skipping tracking tests.") + print(f"\nTracking video '{TRACKING_VIDEO_PATH}' not found. Skipping {label} tracking tests.") return - - # Test 1: Load video - print(f"\n--- Test 1: Load tracking video ---") - success, result = load_tracking_video(TRACKING_VIDEO_PATH) - + + # Test 1: Load video with the requested model + print(f"\n--- {label} Test 1: Load tracking video ---") + success, result = load_tracking_video(TRACKING_VIDEO_PATH, model_name=model_name) + if not success: - print("Failed to load tracking video. Skipping remaining tracking tests.") + print(f"Failed to load tracking video for {label}. Skipping remaining tests.") return - + + # Verify the server loaded the correct model + if result and result.get("model_name") != model_name: + print(f"✗ Server loaded '{result.get('model_name')}' instead of '{model_name}'!") + return + time.sleep(1) - + # Test 2: Track grid - print(f"\n--- Test 2: Track grid of points ---") - success, result = track_grid(grid_size=10, add_support_grid=True) - + print(f"\n--- {label} Test 2: Track grid of points ---") + success, result = track_grid(grid_size=10, add_support_grid=True, expected_mode=mode_key) + if success: - print("\n✓ Grid tracking test completed successfully!") + num_points = result.get("num_points", 0) + num_frames = result.get("num_frames", 0) + if result.get("output_video_path"): + generated_outputs.append(result.get("output_video_path")) + print(f"\n✓ {label} grid tracking test passed ({num_points} pts × {num_frames} frames)") else: - print("\n✗ Grid tracking test failed.") - + print(f"\n✗ {label} grid tracking test failed.") + time.sleep(1) - - # Test 3: Track specific points - print(f"\n--- Test 3: Track specific query points ---") - # Define some query points: [frame, x, y] - # Let's track a few points starting from frame 0 + + # Test 3: Track specific query points (no support grid) + print(f"\n--- {label} Test 3: Track specific query points ---") queries = [ - [0, 400, 350], # Center point - [10, 600, 500], # Upper-left area - [20, 750, 600], # Lower-right area - [30, 900, 200] + [0, 400, 350], + [10, 600, 500], + [20, 750, 600], + [30, 900, 200], ] - success, result = track_points(queries, add_support_grid=False) - + success, result = track_points(queries, add_support_grid=False, expected_mode=mode_key) + if success: - print("\n✓ Point tracking test completed successfully!") + num_points = result.get("num_points", 0) + num_frames = result.get("num_frames", 0) + if result.get("output_video_path"): + generated_outputs.append(result.get("output_video_path")) + print(f"\n✓ {label} point tracking test passed ({num_points} pts × {num_frames} frames)") else: - print("\n✗ Point tracking test failed.") - + print(f"\n✗ {label} point tracking test failed.") + time.sleep(1) - + # Test 4: Track points with support grid - print(f"\n--- Test 4: Track points with support grid ---") + print(f"\n--- {label} Test 4: Track points with support grid ---") queries = [ - [0, 320, 240], # Single point with support grid + [0, 320, 240], ] - success, result = track_points(queries, add_support_grid=True) - + success, result = track_points(queries, add_support_grid=True, expected_mode=mode_key) + if success: - print("\n✓ Point tracking with support grid completed successfully!") + num_points = result.get("num_points", 0) + num_frames = result.get("num_frames", 0) + if result.get("output_video_path"): + generated_outputs.append(result.get("output_video_path")) + print(f"\n✓ {label} point tracking (+ support grid) test passed ({num_points} pts × {num_frames} frames)") else: - print("\n✗ Point tracking with support grid failed.") - + print(f"\n✗ {label} point tracking (+ support grid) test failed.") + + if generated_outputs: + print(f"\n{label} generated outputs:") + for output_path in generated_outputs: + print(f" - {output_path}") + + +def run_tracking_tests_offline(): + """Runs tracking tests using the offline CoTracker model.""" + _run_tracking_suite("cotracker3_offline") + + +def run_tracking_tests_online(): + """Runs tracking tests using the online CoTracker model.""" + _run_tracking_suite("cotracker3_online") + + +def run_tracking_tests(): + """Runs the full tracking test suite (both offline and online).""" + print("\n" + "=" * 60) + print("RUNNING TRACKING TESTS") + print("=" * 60) + + run_tracking_tests_offline() + run_tracking_tests_online() + print("\n" + "=" * 60) print("TRACKING TESTS COMPLETED") print("=" * 60) From 3336290cef38450ab39f768f88b5de96ce9e96d3 Mon Sep 17 00:00:00 2001 From: HarenDev Date: Sat, 21 Mar 2026 16:01:51 -0400 Subject: [PATCH 08/30] update requirements --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index d28f524..74cdbbe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,4 +12,5 @@ opencv-python imutils==0.5.4 mediapy==1.2.2 numpy +scipy git+https://github.com/facebookresearch/co-tracker.git From 4e141ffba28f510c4d9b9eb2b12185773a530307 Mon Sep 17 00:00:00 2001 From: HarenDev Date: Sat, 21 Mar 2026 16:13:36 -0400 Subject: [PATCH 09/30] remove add_support_grid for offline cotracker --- backend/co_tracker.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/co_tracker.py b/backend/co_tracker.py index 834e36f..9ea96f9 100644 --- a/backend/co_tracker.py +++ b/backend/co_tracker.py @@ -216,7 +216,7 @@ def track(self, video: np.ndarray, queries: Optional[np.ndarray] = None, grid_si ) else: pred_tracks, pred_visibility = self._track_offline( - video_torch, queries_torch, grid_size, add_support_grid + video_torch, queries_torch, grid_size ) # pred_tracks: (B, T, N, 2), pred_visibility: (B, T, N) @@ -231,12 +231,11 @@ def track(self, video: np.ndarray, queries: Optional[np.ndarray] = None, grid_si # Offline tracking — single global attention pass over all T frames. # Supports backward_tracking for forward+backward track merging. # ------------------------------------------------------------------ - def _track_offline(self, video_torch, queries_torch, grid_size, add_support_grid): + def _track_offline(self, video_torch, queries_torch, grid_size): if queries_torch is not None: return self.model( video_torch, queries=queries_torch, - add_support_grid=add_support_grid, backward_tracking=True, ) else: From db6ff1ef84d68793bcbd1638e43659736409390e Mon Sep 17 00:00:00 2001 From: HarenDev Date: Thu, 26 Mar 2026 17:33:03 -0400 Subject: [PATCH 10/30] Preparations for disk-based saving of mask frames --- backend/api.py | 51 ++++++++++++++++++++----- backend/utils.py | 96 +++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 133 insertions(+), 14 deletions(-) diff --git a/backend/api.py b/backend/api.py index a2abc50..b9ef8c5 100644 --- a/backend/api.py +++ b/backend/api.py @@ -256,6 +256,7 @@ class VideoPropagateRequest(BaseModel): batch_size: Optional[int] = None online_mode: Optional[bool] = None include_masks_in_response: bool = False + include_saved_mask_paths: bool = False max_frames_in_response: Optional[int] = None max_mask_values_in_response: Optional[int] = None @@ -374,6 +375,28 @@ async def propagate_in_video(request: VideoPropagateRequest): if video_dir is None: return {"error": "Video directory not set. Call /video/init_state first."} + try: + frame_files, masks_dir = prepare_video_masks_output(video_dir) + except Exception as error: + raise HTTPException(status_code=500, detail=f"Failed to prepare mask output directory: {error}") from error + + saved_mask_paths_serializable: dict[int, list[str]] = {} + saved_mask_frame_count = 0 + save_failures = 0 + + def _on_propagated_frame(out_frame_idx: int, frame_masks: dict[int, np.ndarray]): + nonlocal saved_mask_frame_count, save_failures + try: + saved_path = save_single_video_mask_frame(frame_files, masks_dir, int(out_frame_idx), frame_masks) + if saved_path is None: + return + saved_mask_frame_count += 1 + if request.include_saved_mask_paths: + saved_mask_paths_serializable.setdefault(int(out_frame_idx), []).append(saved_path) + except Exception: + save_failures += 1 + logger.exception("Failed to save propagated mask frame %s", out_frame_idx) + try: video_segments = video_masker.propagate_in_video( start_frame_idx=request.start_frame_idx, @@ -381,19 +404,27 @@ async def propagate_in_video(request: VideoPropagateRequest): reverse=request.reverse, batch_size=request.batch_size, online_mode=request.online_mode, + collect_segments=request.include_masks_in_response, + frame_callback=_on_propagated_frame, ) except ValueError as error: raise HTTPException(status_code=400, detail=str(error)) from error - - try: - saved_mask_paths = save_video_masks(video_dir, video_segments) + except torch.OutOfMemoryError as error: + _cleanup_cuda_memory() + raise HTTPException( + status_code=507, + detail="CUDA out of memory during propagation. Try lowering batch_size or enabling CPU offload.", + ) from error + except RuntimeError as error: + if "out of memory" in str(error).lower(): + _cleanup_cuda_memory() + raise HTTPException( + status_code=507, + detail="CUDA out of memory during propagation. Try lowering batch_size or enabling CPU offload.", + ) from error + raise HTTPException(status_code=500, detail=f"Failed to propagate video masks: {error}") from error except Exception as error: - raise HTTPException(status_code=500, detail=f"Failed to save propagated masks: {error}") from error - - saved_mask_paths_serializable = { - int(frame_idx): [str(path) for path in paths] - for frame_idx, paths in saved_mask_paths.items() - } + raise HTTPException(status_code=500, detail=f"Failed to propagate video masks: {error}") from error max_frames_in_response = request.max_frames_in_response if max_frames_in_response is None: @@ -430,6 +461,8 @@ async def propagate_in_video(request: VideoPropagateRequest): "video_segments_returned_frames": video_segments_returned_frames, "video_segments_returned_mask_values": video_segments_returned_mask_values, "video_segments_truncated": video_segments_truncated, + "saved_mask_frame_count": saved_mask_frame_count, + "saved_mask_save_failures": save_failures, "saved_mask_paths": saved_mask_paths_serializable, "online_mode": video_masker.online_mode if request.online_mode is None else bool(request.online_mode), "batch_size": video_masker.default_batch_size if request.batch_size is None else int(request.batch_size), diff --git a/backend/utils.py b/backend/utils.py index 1514710..4c9a28f 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -5,13 +5,33 @@ import hashlib -def show_mask(image, mask, random_color=False, borders=True): - if random_color: +def _color_from_obj_id(obj_id): + """Deterministic BGR color derived from object id.""" + object_id = int(obj_id) + return np.array( + [ + (37 * object_id + 79) % 256, + (67 * object_id + 131) % 256, + (97 * object_id + 191) % 256, + ], + dtype=np.uint8, + ) + + +def show_mask(image, mask, random_color=False, borders=True, color=None): + if color is not None: + color = np.asarray(color, dtype=np.uint8) + elif random_color: color = np.random.randint(0, 256, 3, dtype=np.uint8) else: color = np.array([255, 144, 30], dtype=np.uint8) # BGR for blue + if mask is None: + return image + mask = np.asarray(mask) + if mask.size == 0: + return image mask = np.squeeze(mask) if mask.ndim != 2: @@ -26,9 +46,15 @@ def show_mask(image, mask, random_color=False, borders=True): color_mask = np.zeros((h, w, 3), dtype=np.uint8) color_mask[mask_bool] = color + # Nothing to blend on this frame/object. + if not np.any(mask_bool): + return image + # Blend the colored mask with the original image - # The alpha channel is simulated by weighting - image[mask_bool] = cv2.addWeighted(image[mask_bool], 0.5, color_mask[mask_bool], 0.5, 0) + # Use NumPy blending (safe for any valid selection size). + image_pixels = image[mask_bool].astype(np.float32) + mask_pixels = color_mask[mask_bool].astype(np.float32) + image[mask_bool] = np.clip(0.5 * image_pixels + 0.5 * mask_pixels, 0, 255).astype(np.uint8) if borders: contours, _ = cv2.findContours( @@ -146,7 +172,13 @@ def save_video_masks(video_dir, video_segments): # Apply all object masks to this frame for obj_id, mask in obj_masks.items(): - output_frame = show_mask(output_frame, mask, random_color=True, borders=True) + output_frame = show_mask( + output_frame, + mask, + random_color=False, + borders=True, + color=_color_from_obj_id(obj_id), + ) # Save the frame with masks output_filename = f"frame_{frame_idx:05d}_masks.png" @@ -160,6 +192,60 @@ def save_video_masks(video_dir, video_segments): return saved_paths +def prepare_video_masks_output(video_dir): + """ + Prepare output directory and frame file list for streaming mask writes. + + Returns: + - frame_files: sorted list of frame paths + - masks_dir: output directory path + """ + video_path = Path(video_dir) + masks_dir = video_path / "masks" + + if masks_dir.exists(): + shutil.rmtree(masks_dir) + masks_dir.mkdir(exist_ok=True) + + frame_files = sorted([ + f for f in video_path.iterdir() + if f.suffix.lower() in ['.jpg', '.jpeg', '.png'] + ]) + + return frame_files, masks_dir + + +def save_single_video_mask_frame(frame_files, masks_dir, frame_idx, obj_masks): + """ + Save one propagated mask frame overlay to disk. + + Returns: + - str path to saved file, or None when frame index is out of range / unreadable. + """ + if frame_idx < 0 or frame_idx >= len(frame_files): + return None + + frame_path = frame_files[frame_idx] + frame = cv2.imread(str(frame_path)) + if frame is None: + return None + + output_frame = frame.copy() + for obj_id, mask in obj_masks.items(): + output_frame = show_mask( + output_frame, + mask, + random_color=False, + borders=True, + color=_color_from_obj_id(obj_id), + ) + + output_filename = f"frame_{frame_idx:05d}_masks.png" + output_path = masks_dir / output_filename + cv2.imwrite(str(output_path), output_frame) + return str(output_path) + + def extract_video_to_frames(video_path: Path, output_root: Path, image_extensions: set[str] | None = None) -> Path: """ Extract a video into a cached frame directory. From 63a0ca13bfdc32ba4131d6a8b577c6faafb783ca Mon Sep 17 00:00:00 2001 From: HarenDev <157764758+HarenDev@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:53:47 -0400 Subject: [PATCH 11/30] Added distinction for 20 series and 30+ series gpus --- backend/sam2_video_masker.py | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/backend/sam2_video_masker.py b/backend/sam2_video_masker.py index e204443..8491ed1 100644 --- a/backend/sam2_video_masker.py +++ b/backend/sam2_video_masker.py @@ -27,11 +27,35 @@ def __init__(self): print(f"Utilizing device: {self.device}") if self.device.type == "cuda": - torch.autocast("cuda", dtype=torch.bfloat16).__enter__() - # enable tf32 for new GPUs - if torch.cuda.get_device_properties(0).major >= 8: - torch.backends.cuda.matmul.allow_tf32 = True - torch.backends.cudnn.allow_tf32 = True + device_props = torch.cuda.get_device_properties(0) + gpu_name = device_props.name + compute_capability = (int(device_props.major), int(device_props.minor)) + + # Ampere (RTX 30 series) and newer support TF32 and practical bf16 inference. + is_ampere_or_newer = compute_capability[0] >= 8 + supports_bf16 = bool(getattr(torch.cuda, "is_bf16_supported", lambda: False)()) + use_bf16 = is_ampere_or_newer and supports_bf16 + + autocast_dtype = torch.bfloat16 if use_bf16 else torch.float16 + torch.autocast("cuda", dtype=autocast_dtype).__enter__() + + torch.backends.cuda.matmul.allow_tf32 = is_ampere_or_newer + torch.backends.cudnn.allow_tf32 = is_ampere_or_newer + + if is_ampere_or_newer: + gpu_family = "RTX 30-series+ / Ampere+" + elif compute_capability[0] == 7: + gpu_family = "RTX 20-series / Turing" + else: + gpu_family = "pre-RTX 30 architecture" + + print( + "CUDA precision config | " + f"GPU: {gpu_name} (cc {compute_capability[0]}.{compute_capability[1]}) | " + f"family: {gpu_family} | " + f"autocast: {autocast_dtype} | " + f"tf32: {is_ampere_or_newer}" + ) elif self.device.type == "mps": print( "\nSupport for MPS devices is preliminary. SAM 2 is trained with CUDA and might " From c301d5654f43fb97d8c1ebcee9461cb9b88baa81 Mon Sep 17 00:00:00 2001 From: HarenDev Date: Wed, 22 Apr 2026 15:36:25 -0400 Subject: [PATCH 12/30] Add stateful video mask propagation backend --- backend/api.py | 804 +++++++++++++++++++++++++++++++---- backend/sam2_video_masker.py | 115 +++-- backend/utils.py | 92 ++++ 3 files changed, 903 insertions(+), 108 deletions(-) diff --git a/backend/api.py b/backend/api.py index b9ef8c5..c5ed54e 100644 --- a/backend/api.py +++ b/backend/api.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Optional, Any from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel @@ -15,6 +15,9 @@ import os import torch from urllib.parse import unquote, urlparse +import cv2 +import shutil +import uuid app = FastAPI() @@ -35,12 +38,18 @@ video_frame_files: list[str] = [] tracking_video: Optional[np.ndarray] = None tracking_video_path: Optional[str] = None +video_source_path: Optional[str] = None +video_prompt_events: list[dict[str, Any]] = [] +mask_manifest_path: Optional[str] = None +video_state_epoch: int = 0 PROJECT_ROOT = Path(__file__).resolve().parent.parent IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".bmp"} VIDEO_EXTENSIONS = {".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v"} GENERATED_FRAMES_ROOT = PROJECT_ROOT / "backend/.data_engine_frames" +WINDOW_FRAMES_ROOT = PROJECT_ROOT / "backend/.data_engine_windows" DEFAULT_MAX_MASK_FRAMES_IN_RESPONSE = int(os.getenv("VIDEO_PROPAGATE_MAX_MASK_FRAMES", "0")) DEFAULT_MAX_MASK_VALUES_IN_RESPONSE = int(os.getenv("VIDEO_PROPAGATE_MAX_MASK_VALUES", "0")) +DEFAULT_PROMPT_TRACK_BATCH_SIZE = int(os.getenv("TRACK_PROMPT_BATCH_SIZE", "32")) logger = logging.getLogger(__name__) @@ -51,6 +60,159 @@ def _cleanup_cuda_memory(): torch.cuda.empty_cache() +def _bump_video_state_epoch() -> int: + global video_state_epoch + video_state_epoch += 1 + return video_state_epoch + + +def _reset_video_session_state(): + global video_prompt_events, mask_manifest_path, video_source_path + video_prompt_events = [] + mask_manifest_path = None + video_source_path = None + + +def _record_prompt_event(request: "VideoAddPointsOrBoxRequest"): + global video_prompt_events + + if request.clear_old_points: + video_prompt_events = [ + event + for event in video_prompt_events + if not (event["frame_idx"] == request.frame_idx and event["obj_id"] == request.obj_id) + ] + + event = { + "frame_idx": int(request.frame_idx), + "obj_id": int(request.obj_id), + "points": [list(map(float, point)) for point in (request.points or [])], + "labels": [int(label) for label in (request.labels or [])], + "box": [float(v) for v in request.box] if request.box is not None else None, + "clear_old_points": bool(request.clear_old_points), + } + video_prompt_events.append(event) + + +def _load_video_frames_as_numpy(video_dir_path: Path, frame_file_names: list[str]) -> np.ndarray: + frames_rgb: list[np.ndarray] = [] + for name in frame_file_names: + frame_bgr = cv2.imread(str(video_dir_path / name)) + if frame_bgr is None: + continue + frames_rgb.append(cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB)) + + if not frames_rgb: + raise ValueError(f"No readable frames found under {video_dir_path}") + + return np.stack(frames_rgb, axis=0) + + +def _build_window_dir(frame_paths: list[Path], run_root: Path, window_name: str) -> Path: + window_dir = run_root / window_name + window_dir.mkdir(parents=True, exist_ok=True) + + for local_idx, source_path in enumerate(frame_paths): + target_name = f"{local_idx:05d}{source_path.suffix.lower()}" + target_path = window_dir / target_name + try: + os.symlink(source_path, target_path) + except OSError: + try: + os.link(source_path, target_path) + except OSError: + shutil.copy2(source_path, target_path) + + return window_dir + + +def _manifest_frame_payload(frame_masks: dict[int, np.ndarray]) -> dict[str, Any]: + objects: dict[str, Any] = {} + for obj_id, mask in frame_masks.items(): + mask_array = np.asarray(mask).astype(bool) + if mask_array.ndim != 2: + mask_array = np.squeeze(mask_array) + if mask_array.ndim != 2: + continue + objects[str(int(obj_id))] = { + "size": [int(mask_array.shape[0]), int(mask_array.shape[1])], + "rle": encode_mask_to_rle(mask_array), + "bbox": mask_bbox_xywh(mask_array), + } + return {"objects": objects} + + +def _mask_logits_to_2d_bool(mask_logits: Any) -> np.ndarray: + mask_array = (mask_logits > 0.0).detach().cpu().numpy() + mask_array = np.squeeze(mask_array).astype(bool) + if mask_array.ndim == 3 and mask_array.shape[0] == 1: + mask_array = np.squeeze(mask_array, axis=0) + if mask_array.ndim != 2: + raise ValueError(f"Expected 2D mask after squeeze, got shape {mask_array.shape}") + return mask_array + + +def _ensure_tracker_model(model_name: str): + global tracker + if tracker is None or getattr(tracker, "model_name", None) != model_name: + if tracker is not None: + del tracker + _cleanup_cuda_memory() + tracker = cot.CoTracker(model_name=model_name) + + +def _restore_video_masker_from_prompt_events( + *, + online_mode: bool, + batch_size: Optional[int], + offload_video_to_cpu: Optional[bool], + offload_state_to_cpu: Optional[bool], + increment_epoch: bool = True, +) -> None: + global video_masker, video_dir, video_prompt_events + + if video_dir is None: + return + + if video_masker is None: + video_masker = svm.SAM2VideoMasker() + + video_masker.init_state( + video_dir, + online_mode=online_mode, + batch_size=batch_size, + offload_video_to_cpu=offload_video_to_cpu, + offload_state_to_cpu=offload_state_to_cpu, + async_loading_frames=False, + ) + + for event in video_prompt_events: + points = event["points"] if event["points"] else None + labels = event["labels"] if event["labels"] else None + video_masker.add_new_points_or_box( + frame_idx=int(event["frame_idx"]), + obj_id=int(event["obj_id"]), + points=points, + labels=labels, + clear_old_points=bool(event.get("clear_old_points", True)), + box=event.get("box"), + ) + + if increment_epoch: + _bump_video_state_epoch() + + +def _load_tracking_video_from_current_video_state() -> tuple[np.ndarray, str]: + global video_dir, video_frame_files + if video_dir is None or not video_frame_files: + raise ValueError("Video is not initialized for tracking.") + + # Keep tracking frame indices aligned with the exact frame sequence used by masking/frontend. + # Decoding directly from source_video_path can introduce index drift across different decoders. + frame_video = _load_video_frames_as_numpy(Path(video_dir), video_frame_files) + return frame_video, str(video_dir) + + def _normalize_input_path(path_value: str) -> str: normalized = str(path_value).strip().strip('"').strip("'") if not normalized: @@ -118,6 +280,8 @@ def _prepare_video_masker_for_video_init(): if video_masker is None: video_masker = svm.SAM2VideoMasker() + _reset_video_session_state() + def _initialize_video_state_from_resolved_input( resolved_input_path: Path, @@ -128,7 +292,7 @@ def _initialize_video_state_from_resolved_input( offload_state_to_cpu: Optional[bool], async_loading_frames: bool, ): - global video_masker, video_dir, video_frame_files + global video_masker, video_dir, video_frame_files, video_source_path source_video_path = None if resolved_input_path.is_file(): @@ -185,6 +349,9 @@ def _initialize_video_state_from_resolved_input( detail=f"No image frames found in directory: {resolved_video_dir}" ) + video_source_path = source_video_path + state_epoch = _bump_video_state_epoch() + return { "message": "Video state initialized successfully", "num_frames": len(video_frame_files), @@ -194,6 +361,7 @@ def _initialize_video_state_from_resolved_input( "batch_size": video_masker.default_batch_size, "offload_video_to_cpu": video_masker.offload_video_to_cpu, "offload_state_to_cpu": video_masker.offload_state_to_cpu, + "state_epoch": state_epoch, } @@ -282,6 +450,11 @@ class TrackingPointsRequest(BaseModel): add_support_grid: bool = True +class TrackingPromptPointsRequest(BaseModel): + model_name: str = "cotracker3_online" + add_support_grid: bool = True + + @app.get("/") async def root(): return {"message": "Data Engine Backend"} @@ -324,14 +497,27 @@ async def reset_video_state(): if video_masker is None: return {"error": "Video masker not active."} video_masker.reset_state() - return {"message": "Video state reset successfully"} + _reset_video_session_state() + return { + "message": "Video state reset successfully", + "state_epoch": _bump_video_state_epoch(), + } @app.post("/video/add_new_points_or_box") async def add_new_points_or_box(request: VideoAddPointsOrBoxRequest): - global video_masker + global video_masker, video_frame_files, video_state_epoch if video_masker is None: return {"error": "Video masker not active."} - out_obj_ids, out_mask_logits = video_masker.add_new_points_or_box( + + if not video_frame_files: + raise HTTPException(status_code=400, detail="No video frames available. Call /video/init_state first.") + if request.frame_idx < 0 or request.frame_idx >= len(video_frame_files): + raise HTTPException( + status_code=400, + detail=f"Frame index out of bounds: {request.frame_idx}. Expected 0..{len(video_frame_files) - 1}.", + ) + + out_frame_idx, out_obj_ids, out_mask_logits = video_masker.add_new_points_or_box( frame_idx=request.frame_idx, obj_id=request.obj_id, points=request.points, @@ -339,10 +525,91 @@ async def add_new_points_or_box(request: VideoAddPointsOrBoxRequest): clear_old_points=request.clear_old_points, box=request.box ) - masks_list = [(out_mask_logits[i] > 0.0).squeeze(0).cpu().numpy().tolist() for i in range(len(out_obj_ids))] + returned_frame_idx = int(out_frame_idx) + if returned_frame_idx != int(request.frame_idx): + raise HTTPException( + status_code=409, + detail=( + "Frame mismatch in SAM2 response: " + f"request_frame_idx={int(request.frame_idx)} response_frame_idx={returned_frame_idx}" + ), + ) + + normalized_obj_ids = [int(obj_id) for obj_id in out_obj_ids] + if len(normalized_obj_ids) != len(out_mask_logits): + raise HTTPException( + status_code=500, + detail=( + "Invalid SAM2 response: " + f"{len(normalized_obj_ids)} object IDs but {len(out_mask_logits)} mask tensors." + ), + ) + + masks_list: list[list[list[bool]]] = [] + mask_pixel_counts: dict[int, int] = {} + mask_shapes: dict[int, list[int]] = {} + for index, obj_id in enumerate(normalized_obj_ids): + mask_2d = _mask_logits_to_2d_bool(out_mask_logits[index]) + masks_list.append(mask_2d.tolist()) + mask_pixel_counts[int(obj_id)] = int(np.count_nonzero(mask_2d)) + mask_shapes[int(obj_id)] = [int(mask_2d.shape[0]), int(mask_2d.shape[1])] + + selected_obj_id = int(request.obj_id) + selected_obj_index = normalized_obj_ids.index(selected_obj_id) if selected_obj_id in normalized_obj_ids else None + selected_obj_pixels = int(mask_pixel_counts.get(selected_obj_id, 0)) + has_positive_prompt = any(int(label) == 1 for label in (request.labels or [])) + used_single_frame_fallback = False + + # Some interactive clicks return an empty mask before memory preflight/consolidation. + # If the selected object mask is empty, run a 1-frame propagate pass as a bounded fallback. + if selected_obj_index is not None and selected_obj_pixels == 0 and has_positive_prompt: + try: + fallback_segments = video_masker.propagate_in_video( + start_frame_idx=int(request.frame_idx), + max_frame_num_to_track=1, + reverse=False, + batch_size=1, + online_mode=video_masker.online_mode, + collect_segments=True, + ) + fallback_frame_masks = fallback_segments.get(int(request.frame_idx), {}) + fallback_mask = fallback_frame_masks.get(selected_obj_id) + if fallback_mask is not None: + fallback_mask_2d = np.asarray(fallback_mask).astype(bool) + fallback_mask_2d = np.squeeze(fallback_mask_2d) + if fallback_mask_2d.ndim == 2: + fallback_pixels = int(np.count_nonzero(fallback_mask_2d)) + if fallback_pixels > 0: + masks_list[selected_obj_index] = fallback_mask_2d.tolist() + mask_pixel_counts[selected_obj_id] = fallback_pixels + mask_shapes[selected_obj_id] = [int(fallback_mask_2d.shape[0]), int(fallback_mask_2d.shape[1])] + used_single_frame_fallback = True + except Exception: + logger.exception( + "Single-frame interactive fallback failed for frame=%s obj=%s", + int(request.frame_idx), + selected_obj_id, + ) + + _record_prompt_event(request) + logger.info( + "Interactive mask response frame=%s obj=%s pixels=%s fallback=%s", + int(request.frame_idx), + selected_obj_id, + int(mask_pixel_counts.get(selected_obj_id, 0)), + used_single_frame_fallback, + ) + return { - "out_obj_ids": out_obj_ids, - "out_masks": masks_list + "request_frame_idx": int(request.frame_idx), + "frame_idx": returned_frame_idx, + "frame_file": video_frame_files[returned_frame_idx], + "out_obj_ids": normalized_obj_ids, + "out_masks": masks_list, + "mask_pixel_counts": mask_pixel_counts, + "mask_shapes": mask_shapes, + "single_frame_fallback_used": used_single_frame_fallback, + "state_epoch": int(video_state_epoch), } @app.post("/video/add_new_mask") @@ -360,7 +627,7 @@ async def add_new_mask(request: VideoAddMaskRequest): mask=mask ) - masks_list = [(out_mask_logits[i] > 0.0).squeeze(0).cpu().numpy().tolist() for i in range(len(out_obj_ids))] + masks_list = [_mask_logits_to_2d_bool(out_mask_logits[i]).tolist() for i in range(len(out_obj_ids))] return { "frame_idx": frame_idx, "out_obj_ids": out_obj_ids, @@ -369,44 +636,196 @@ async def add_new_mask(request: VideoAddMaskRequest): @app.post("/video/propagate_in_video") async def propagate_in_video(request: VideoPropagateRequest): - global video_masker, video_dir + global video_masker, video_dir, video_frame_files, mask_manifest_path, video_state_epoch if video_masker is None: return {"error": "Video masker not active."} if video_dir is None: return {"error": "Video directory not set. Call /video/init_state first."} - - try: - frame_files, masks_dir = prepare_video_masks_output(video_dir) - except Exception as error: - raise HTTPException(status_code=500, detail=f"Failed to prepare mask output directory: {error}") from error + + if request.reverse: + raise HTTPException( + status_code=400, + detail="Reverse propagation is not supported in half-window mode.", + ) + + if not video_frame_files: + raise HTTPException(status_code=400, detail="No video frames available. Call /video/init_state first.") + + if not video_prompt_events: + raise HTTPException(status_code=400, detail="No prompts available for propagation.") + + effective_online_mode = video_masker.online_mode if request.online_mode is None else bool(request.online_mode) + effective_batch_size = video_masker.default_batch_size if request.batch_size is None else int(request.batch_size) + effective_offload_video_to_cpu = video_masker.offload_video_to_cpu + effective_offload_state_to_cpu = video_masker.offload_state_to_cpu + if effective_batch_size <= 0: + raise HTTPException(status_code=400, detail="batch_size must be a positive integer.") + + num_frames = len(video_frame_files) + if request.start_frame_idx is not None: + start_frame_idx = int(request.start_frame_idx) + else: + start_frame_idx = min(int(event["frame_idx"]) for event in video_prompt_events) + start_frame_idx = min(max(start_frame_idx, 0), num_frames - 1) + + if request.max_frame_num_to_track is None: + end_frame_idx = num_frames - 1 + else: + requested = int(request.max_frame_num_to_track) + if requested <= 0: + return { + "video_segments": {}, + "video_segments_total_frames": 0, + "video_segments_returned_frames": 0, + "video_segments_returned_mask_values": 0, + "video_segments_truncated": False, + "saved_mask_frame_count": 0, + "saved_mask_save_failures": 0, + "saved_mask_paths": {}, + "online_mode": effective_online_mode, + "batch_size": effective_batch_size, + "mask_manifest_path": mask_manifest_path, + "state_epoch": int(video_state_epoch), + } + end_frame_idx = min(num_frames - 1, start_frame_idx + requested - 1) + + if end_frame_idx < start_frame_idx: + raise HTTPException(status_code=400, detail="Invalid propagation frame range.") + + frame_files, masks_dir = prepare_video_masks_output(video_dir) + manifest_file_path = masks_dir / "manifest.json" + + first_frame = cv2.imread(str(Path(video_dir) / video_frame_files[start_frame_idx])) + if first_frame is None: + raise HTTPException(status_code=500, detail="Unable to read first frame for manifest metadata.") + + manifest = build_empty_mask_manifest( + source_video_path=video_source_path, + resolved_video_frames_dir=str(video_dir), + num_frames=num_frames, + frame_height=int(first_frame.shape[0]), + frame_width=int(first_frame.shape[1]), + ) + manifest_frames: dict[str, Any] = manifest["frames"] + + split_frame_idx = (start_frame_idx + end_frame_idx) // 2 + windows: list[tuple[int, int]] = [(start_frame_idx, split_frame_idx)] + if split_frame_idx < end_frame_idx: + windows.append((split_frame_idx, end_frame_idx)) + + run_root = WINDOW_FRAMES_ROOT / f"run_{uuid.uuid4().hex[:12]}" + run_root.mkdir(parents=True, exist_ok=True) saved_mask_paths_serializable: dict[int, list[str]] = {} saved_mask_frame_count = 0 save_failures = 0 + processed_frames: set[int] = set() + boundary_masks: dict[int, np.ndarray] = {} + boundary_frame_idx: Optional[int] = None - def _on_propagated_frame(out_frame_idx: int, frame_masks: dict[int, np.ndarray]): - nonlocal saved_mask_frame_count, save_failures - try: - saved_path = save_single_video_mask_frame(frame_files, masks_dir, int(out_frame_idx), frame_masks) - if saved_path is None: - return - saved_mask_frame_count += 1 - if request.include_saved_mask_paths: - saved_mask_paths_serializable.setdefault(int(out_frame_idx), []).append(saved_path) - except Exception: - save_failures += 1 - logger.exception("Failed to save propagated mask frame %s", out_frame_idx) + video_segments_serializable: dict[int, dict[int, list]] = {} try: - video_segments = video_masker.propagate_in_video( - start_frame_idx=request.start_frame_idx, - max_frame_num_to_track=request.max_frame_num_to_track, - reverse=request.reverse, - batch_size=request.batch_size, - online_mode=request.online_mode, - collect_segments=request.include_masks_in_response, - frame_callback=_on_propagated_frame, - ) + for window_index, (window_start, window_end) in enumerate(windows): + window_frame_paths = [Path(video_dir) / video_frame_files[idx] for idx in range(window_start, window_end + 1)] + window_name = f"window_{window_index}_{window_start}_{window_end}" + window_dir = _build_window_dir(window_frame_paths, run_root, window_name) + + video_masker.init_state( + str(window_dir), + online_mode=effective_online_mode, + batch_size=effective_batch_size, + offload_video_to_cpu=effective_offload_video_to_cpu, + offload_state_to_cpu=effective_offload_state_to_cpu, + async_loading_frames=False, + ) + + if window_index > 0 and boundary_masks and boundary_frame_idx is not None: + local_boundary_idx = int(boundary_frame_idx - window_start) + for obj_id, obj_mask in boundary_masks.items(): + video_masker.add_new_mask( + frame_idx=local_boundary_idx, + obj_id=int(obj_id), + mask=np.asarray(obj_mask).astype(bool), + ) + + for event in video_prompt_events: + event_frame_idx = int(event["frame_idx"]) + if event_frame_idx < window_start or event_frame_idx > window_end: + continue + local_event_frame_idx = event_frame_idx - window_start + points = event["points"] if event["points"] else None + labels = event["labels"] if event["labels"] else None + video_masker.add_new_points_or_box( + frame_idx=local_event_frame_idx, + obj_id=int(event["obj_id"]), + points=points, + labels=labels, + clear_old_points=bool(event.get("clear_old_points", True)), + box=event.get("box"), + ) + + local_start_frame = max(start_frame_idx, window_start) - window_start + local_max_frames = (window_end - window_start + 1) - local_start_frame + + def _on_window_frame(local_frame_idx: int, frame_masks: dict[int, np.ndarray]): + nonlocal saved_mask_frame_count, save_failures, boundary_masks, boundary_frame_idx + global_frame_idx = int(window_start + local_frame_idx) + if global_frame_idx < start_frame_idx or global_frame_idx > end_frame_idx: + return + + is_overlap_duplicate = ( + window_index > 0 + and global_frame_idx == window_start + and global_frame_idx in processed_frames + ) + if is_overlap_duplicate: + boundary_masks = { + int(obj_id): np.asarray(mask).astype(bool) + for obj_id, mask in frame_masks.items() + } + boundary_frame_idx = global_frame_idx + return + + processed_frames.add(global_frame_idx) + manifest_frames[str(global_frame_idx)] = _manifest_frame_payload(frame_masks) + if request.include_masks_in_response: + video_segments_serializable[global_frame_idx] = { + int(obj_id): np.asarray(mask).astype(bool).tolist() + for obj_id, mask in frame_masks.items() + } + + try: + saved_path = save_single_video_mask_frame( + frame_files, + masks_dir, + global_frame_idx, + frame_masks, + ) + if saved_path is not None: + saved_mask_frame_count += 1 + if request.include_saved_mask_paths: + saved_mask_paths_serializable.setdefault(global_frame_idx, []).append(saved_path) + except Exception: + save_failures += 1 + logger.exception("Failed to save propagated mask frame %s", global_frame_idx) + + if global_frame_idx == window_end: + boundary_masks = { + int(obj_id): np.asarray(mask).astype(bool) + for obj_id, mask in frame_masks.items() + } + boundary_frame_idx = global_frame_idx + + video_masker.propagate_in_video( + start_frame_idx=local_start_frame, + max_frame_num_to_track=local_max_frames, + reverse=False, + batch_size=effective_batch_size, + online_mode=effective_online_mode, + collect_segments=False, + frame_callback=_on_window_frame, + ) except ValueError as error: raise HTTPException(status_code=400, detail=str(error)) from error except torch.OutOfMemoryError as error: @@ -425,63 +844,65 @@ def _on_propagated_frame(out_frame_idx: int, frame_masks: dict[int, np.ndarray]) raise HTTPException(status_code=500, detail=f"Failed to propagate video masks: {error}") from error except Exception as error: raise HTTPException(status_code=500, detail=f"Failed to propagate video masks: {error}") from error + finally: + shutil.rmtree(run_root, ignore_errors=True) - max_frames_in_response = request.max_frames_in_response - if max_frames_in_response is None: - max_frames_in_response = DEFAULT_MAX_MASK_FRAMES_IN_RESPONSE - - max_mask_values_in_response = request.max_mask_values_in_response - if max_mask_values_in_response is None: - max_mask_values_in_response = DEFAULT_MAX_MASK_VALUES_IN_RESPONSE + write_mask_manifest(manifest_file_path, manifest) + mask_manifest_path = str(manifest_file_path) - video_segments_serializable: dict[int, dict[int, list]] = {} - video_segments_truncated = False - video_segments_returned_frames = 0 - video_segments_returned_mask_values = 0 - - if request.include_masks_in_response: - try: - video_segments_serializable, video_segments_truncated, video_segments_returned_frames, video_segments_returned_mask_values = _serialize_video_segments_for_response( - video_segments, - max_frames=max_frames_in_response, - max_mask_values=max_mask_values_in_response, - ) - except MemoryError: - logger.warning("Mask serialization skipped due to memory pressure.") - video_segments_serializable = {} - video_segments_truncated = len(video_segments) > 0 - except Exception: - logger.exception("Mask serialization failed; returning saved mask paths only.") - video_segments_serializable = {} - video_segments_truncated = len(video_segments) > 0 + try: + # Rebind interactive state to the original full video frame index space. + _restore_video_masker_from_prompt_events( + online_mode=effective_online_mode, + batch_size=effective_batch_size, + offload_video_to_cpu=effective_offload_video_to_cpu, + offload_state_to_cpu=effective_offload_state_to_cpu, + ) + except Exception as error: + raise HTTPException( + status_code=500, + detail=f"Propagation completed but failed to restore interactive masking state: {error}", + ) from error return { - "video_segments": video_segments_serializable, - "video_segments_total_frames": len(video_segments), - "video_segments_returned_frames": video_segments_returned_frames, - "video_segments_returned_mask_values": video_segments_returned_mask_values, - "video_segments_truncated": video_segments_truncated, + "video_segments": video_segments_serializable if request.include_masks_in_response else {}, + "video_segments_total_frames": len(processed_frames), + "video_segments_returned_frames": len(video_segments_serializable), + "video_segments_returned_mask_values": 0, + "video_segments_truncated": False, "saved_mask_frame_count": saved_mask_frame_count, "saved_mask_save_failures": save_failures, "saved_mask_paths": saved_mask_paths_serializable, - "online_mode": video_masker.online_mode if request.online_mode is None else bool(request.online_mode), - "batch_size": video_masker.default_batch_size if request.batch_size is None else int(request.batch_size), + "online_mode": effective_online_mode, + "batch_size": effective_batch_size, + "mask_manifest_path": mask_manifest_path, + "state_epoch": int(video_state_epoch), } @app.post("/video/clear_all_prompts_in_frame") async def clear_all_prompts_in_frame(frame_idx: int, obj_id: int): - global video_masker + global video_masker, video_prompt_events if video_masker is None: return {"error": "Video masker not active."} video_masker.clear_all_prompts_in_frame(frame_idx, obj_id) + video_prompt_events = [ + event + for event in video_prompt_events + if not (int(event["frame_idx"]) == int(frame_idx) and int(event["obj_id"]) == int(obj_id)) + ] return {"message": "Cleared all prompts in frame successfully"} @app.post("/video/remove_object") async def remove_object(obj_id: int): - global video_masker + global video_masker, video_prompt_events if video_masker is None: return {"error": "Video masker not active."} video_masker.remove_object(obj_id) + video_prompt_events = [ + event + for event in video_prompt_events + if int(event["obj_id"]) != int(obj_id) + ] return {"message": "Object removed successfully"} @@ -495,6 +916,7 @@ async def load_tracking_video(request: TrackingLoadVideoRequest): del video_masker video_masker = None video_dir = None + _bump_video_state_epoch() _cleanup_cuda_memory() # (Re-)initialise tracker with the requested model variant. @@ -521,6 +943,197 @@ async def load_tracking_video(request: TrackingLoadVideoRequest): } +@app.post("/tracking/track_prompt_points") +async def track_prompt_points(request: TrackingPromptPointsRequest): + global tracker, tracking_video, tracking_video_path, video_masker, video_prompt_events, video_dir, video_state_epoch + + if not video_prompt_events: + raise HTTPException(status_code=400, detail="No annotation prompts available for tracking.") + + positive_queries: list[list[float]] = [] + point_metadata: list[dict[str, Any]] = [] + + for event_idx, event in enumerate(video_prompt_events): + points = event.get("points", []) or [] + labels = event.get("labels", []) or [1] * len(points) + frame_idx = int(event.get("frame_idx", 0)) + obj_id = int(event.get("obj_id", 0)) + for point_idx, point in enumerate(points): + if point_idx >= len(labels) or int(labels[point_idx]) != 1: + continue + if len(point) < 2: + continue + x_coord = float(point[0]) + y_coord = float(point[1]) + positive_queries.append([float(frame_idx), x_coord, y_coord]) + point_metadata.append( + { + "point_id": f"p{event_idx}_{point_idx}", + "obj_id": obj_id, + "source_frame_idx": frame_idx, + "source_x": x_coord, + "source_y": y_coord, + } + ) + + if not positive_queries: + raise HTTPException(status_code=400, detail="No positive prompt points available for tracking.") + + should_restore_video_masker = video_masker is not None and video_dir is not None + restore_online_mode = video_masker.online_mode if video_masker is not None else True + restore_batch_size = video_masker.default_batch_size if video_masker is not None else None + restore_offload_video_to_cpu = video_masker.offload_video_to_cpu if video_masker is not None else None + restore_offload_state_to_cpu = video_masker.offload_state_to_cpu if video_masker is not None else None + + def _restore_masker_state(*, raise_on_error: bool) -> None: + if not should_restore_video_masker: + return + try: + _restore_video_masker_from_prompt_events( + online_mode=restore_online_mode, + batch_size=restore_batch_size, + offload_video_to_cpu=restore_offload_video_to_cpu, + offload_state_to_cpu=restore_offload_state_to_cpu, + ) + except Exception as error: + logger.exception("Failed to restore interactive video masker state after prompt tracking") + if raise_on_error: + raise HTTPException( + status_code=500, + detail=f"Prompt-point tracking finished but failed to restore interactive masking state: {error}", + ) from error + + if video_masker is not None: + del video_masker + video_masker = None + _bump_video_state_epoch() + _cleanup_cuda_memory() + + _ensure_tracker_model(request.model_name) + + def _is_oom_runtime_error(error: RuntimeError) -> bool: + return "out of memory" in str(error).lower() + + def _track_queries_batched( + video: np.ndarray, + query_array: np.ndarray, + *, + add_support_grid: bool, + ) -> tuple[np.ndarray, np.ndarray]: + if query_array.size == 0: + raise ValueError("No query points provided for tracking.") + + initial_batch_size = int(DEFAULT_PROMPT_TRACK_BATCH_SIZE) + if initial_batch_size <= 0: + initial_batch_size = query_array.shape[0] + batch_size = max(1, min(initial_batch_size, int(query_array.shape[0]))) + + tracks_batches: list[np.ndarray] = [] + visibility_batches: list[np.ndarray] = [] + index = 0 + + while index < query_array.shape[0]: + end_index = min(query_array.shape[0], index + batch_size) + query_batch = query_array[index:end_index] + + try: + batch_tracks, batch_visibility = tracker.track( + video, + queries=query_batch, + add_support_grid=add_support_grid, + ) + except torch.OutOfMemoryError as error: + _cleanup_cuda_memory() + if batch_size <= 1: + raise error + batch_size = max(1, batch_size // 2) + continue + except RuntimeError as error: + if _is_oom_runtime_error(error): + _cleanup_cuda_memory() + if batch_size <= 1: + raise error + batch_size = max(1, batch_size // 2) + continue + raise + + tracks_batches.append(batch_tracks) + visibility_batches.append(batch_visibility) + index = end_index + + tracks = np.concatenate(tracks_batches, axis=0) + visibility = np.concatenate(visibility_batches, axis=0) + return tracks, visibility + + try: + tracking_video, tracking_video_path = _load_tracking_video_from_current_video_state() + query_array = np.asarray(positive_queries, dtype=np.float32) + support_grid_used = bool(request.add_support_grid) + + try: + tracks, visibility = _track_queries_batched( + tracking_video, + query_array, + add_support_grid=support_grid_used, + ) + except torch.OutOfMemoryError: + if not support_grid_used: + raise + support_grid_used = False + tracks, visibility = _track_queries_batched( + tracking_video, + query_array, + add_support_grid=False, + ) + except RuntimeError as error: + if not _is_oom_runtime_error(error) or not support_grid_used: + raise + support_grid_used = False + tracks, visibility = _track_queries_batched( + tracking_video, + query_array, + add_support_grid=False, + ) + except ValueError as error: + _restore_masker_state(raise_on_error=False) + raise HTTPException(status_code=400, detail=str(error)) from error + except torch.OutOfMemoryError as error: + _cleanup_cuda_memory() + _restore_masker_state(raise_on_error=False) + raise HTTPException( + status_code=507, + detail="CUDA out of memory during tracking. Try online mode or fewer prompt points.", + ) from error + except RuntimeError as error: + if "out of memory" in str(error).lower(): + _cleanup_cuda_memory() + _restore_masker_state(raise_on_error=False) + raise HTTPException( + status_code=507, + detail="CUDA out of memory during tracking. Try online mode or fewer prompt points.", + ) from error + _restore_masker_state(raise_on_error=False) + raise HTTPException(status_code=500, detail=f"Prompt-point tracking failed: {error}") from error + except Exception as error: + _restore_masker_state(raise_on_error=False) + raise HTTPException(status_code=500, detail=f"Prompt-point tracking failed: {error}") from error + + _cleanup_cuda_memory() + _restore_masker_state(raise_on_error=True) + + return { + "message": "Prompt-point tracking completed", + "model_name": tracker.model_name, + "num_points": int(tracks.shape[0]), + "num_frames": int(tracks.shape[1]), + "add_support_grid_used": support_grid_used, + "tracks": tracks.tolist(), + "visibility": visibility.tolist(), + "points": point_metadata, + "state_epoch": int(video_state_epoch), + } + + @app.post("/tracking/track_grid") async def track_grid(request: TrackingGridRequest): """Track a grid of points across the video.""" @@ -665,6 +1278,53 @@ async def get_video_info(): "frame_files": video_frame_files } + +@app.get("/video/mask_manifest") +async def get_mask_manifest(): + global video_dir, mask_manifest_path + if video_dir is None: + return {"error": "Video not initialized"} + + manifest_path = Path(mask_manifest_path) if mask_manifest_path else Path(video_dir) / "masks" / "manifest.json" + if not manifest_path.exists(): + return {"error": "Mask manifest not found. Run /video/propagate_in_video first."} + + manifest = load_mask_manifest(manifest_path) + return { + "version": manifest.get("version"), + "source_video_path": manifest.get("source_video_path"), + "resolved_video_frames_dir": manifest.get("resolved_video_frames_dir"), + "num_frames": manifest.get("num_frames", 0), + "frame_height": manifest.get("frame_height"), + "frame_width": manifest.get("frame_width"), + "mask_manifest_path": str(manifest_path), + } + + +@app.get("/video/mask_data/{frame_idx}") +async def get_mask_data(frame_idx: int): + global video_dir, mask_manifest_path + if video_dir is None: + return {"error": "Video not initialized"} + if frame_idx < 0: + return {"error": "Frame index out of bounds"} + + manifest_path = Path(mask_manifest_path) if mask_manifest_path else Path(video_dir) / "masks" / "manifest.json" + if not manifest_path.exists(): + return {"frame_idx": frame_idx, "objects": {}} + + manifest = load_mask_manifest(manifest_path) + num_frames = int(manifest.get("num_frames", 0)) + if frame_idx >= num_frames: + return {"error": "Frame index out of bounds"} + + frame_payload = manifest.get("frames", {}).get(str(frame_idx), {"objects": {}}) + objects_payload = frame_payload.get("objects", {}) + return { + "frame_idx": int(frame_idx), + "objects": objects_payload, + } + @app.get("/video/frame/{frame_idx}") async def get_video_frame(frame_idx: int): global video_dir, video_frame_files diff --git a/backend/sam2_video_masker.py b/backend/sam2_video_masker.py index 8491ed1..2dab4b0 100644 --- a/backend/sam2_video_masker.py +++ b/backend/sam2_video_masker.py @@ -1,6 +1,7 @@ import torch import numpy as np import os +import gc from pathlib import Path from sam2.sam2_video_predictor import SAM2VideoPredictor from utils import extract_video_to_frames @@ -90,7 +91,7 @@ def init_state( if offload_video_to_cpu is None: offload_video_to_cpu = self.online_mode if offload_state_to_cpu is None: - offload_state_to_cpu = False + offload_state_to_cpu = self.online_mode self.offload_video_to_cpu = bool(offload_video_to_cpu) self.offload_state_to_cpu = bool(offload_state_to_cpu) @@ -135,7 +136,7 @@ def reset_state(self): self.predictor.reset_state(self.inference_state) def add_new_points_or_box(self, frame_idx, obj_id, points=None, labels=None, clear_old_points=True, box=None): - _, out_obj_ids, out_mask_logits = self.predictor.add_new_points_or_box( + out_frame_idx, out_obj_ids, out_mask_logits = self.predictor.add_new_points_or_box( inference_state=self.inference_state, frame_idx=frame_idx, obj_id=obj_id, @@ -145,7 +146,7 @@ def add_new_points_or_box(self, frame_idx, obj_id, points=None, labels=None, cle box=box, ) - return out_obj_ids, out_mask_logits + return out_frame_idx, out_obj_ids, out_mask_logits def add_new_mask(self, frame_idx, obj_id, mask): """Add new mask to a frame.""" @@ -181,23 +182,28 @@ def _purge_non_conditioning_outputs(self, anchor_frame_idx, reverse=False): lower_bound = anchor_frame_idx - keep_window upper_bound = anchor_frame_idx + keep_window - output_dict_per_obj = self.inference_state.get("output_dict_per_obj", {}) - for obj_output_dict in output_dict_per_obj.values(): - non_cond_outputs = obj_output_dict.get("non_cond_frame_outputs", {}) - if reverse: - stale_keys = [ - frame_idx - for frame_idx in list(non_cond_outputs.keys()) - if frame_idx < anchor_frame_idx or frame_idx > upper_bound - ] - else: - stale_keys = [ - frame_idx - for frame_idx in list(non_cond_outputs.keys()) - if frame_idx > anchor_frame_idx or frame_idx < lower_bound - ] - for frame_idx in stale_keys: - non_cond_outputs.pop(frame_idx, None) + def _purge_non_cond_dict(per_obj_dict): + for obj_output_dict in per_obj_dict.values(): + non_cond_outputs = obj_output_dict.get("non_cond_frame_outputs", {}) + if reverse: + stale_keys = [ + frame_idx + for frame_idx in list(non_cond_outputs.keys()) + if frame_idx < anchor_frame_idx or frame_idx > upper_bound + ] + else: + stale_keys = [ + frame_idx + for frame_idx in list(non_cond_outputs.keys()) + if frame_idx > anchor_frame_idx or frame_idx < lower_bound + ] + for frame_idx in stale_keys: + non_cond_outputs.pop(frame_idx, None) + + _purge_non_cond_dict(self.inference_state.get("output_dict_per_obj", {})) + _purge_non_cond_dict(self.inference_state.get("temp_output_dict_per_obj", {})) + + gc.collect() if self.device.type == "cuda": torch.cuda.empty_cache() @@ -244,6 +250,8 @@ def _propagate_in_video_batched( max_frame_num_to_track=None, reverse=False, batch_size=None, + collect_segments=True, + frame_callback=None, ): if self.inference_state is None: return {} @@ -265,7 +273,7 @@ def _propagate_in_video_batched( return {} total_remaining = total_frames_to_process - video_segments = {} + video_segments = {} if collect_segments else None global_last_processed_frame_idx = None while True: @@ -282,20 +290,47 @@ def _propagate_in_video_batched( last_processed_frame_idx = None processed_before_batch = total_frames_to_process - total_remaining - for out_frame_idx, out_obj_ids, out_mask_logits in self.predictor.propagate_in_video( - self.inference_state, - start_frame_idx=current_start, - max_frame_num_to_track=predictor_max_frames, - reverse=reverse, - progress_total=total_frames_to_process, - progress_initial=processed_before_batch, - ): - if out_frame_idx not in video_segments: + propagate_kwargs = { + "start_frame_idx": current_start, + "max_frame_num_to_track": predictor_max_frames, + "reverse": reverse, + } + + def _iter_with_progress_fallback(): + try: + yield from self.predictor.propagate_in_video( + self.inference_state, + progress_total=total_frames_to_process, + progress_initial=processed_before_batch, + **propagate_kwargs, + ) + return + except TypeError as error: + # Backward compatibility with SAM2 predictor builds that do + # not support progress_* kwargs. + error_message = str(error) + if "progress_total" not in error_message and "progress_initial" not in error_message: + raise + + yield from self.predictor.propagate_in_video( + self.inference_state, + **propagate_kwargs, + ) + + for out_frame_idx, out_obj_ids, out_mask_logits in _iter_with_progress_fallback(): + if collect_segments: + if out_frame_idx not in video_segments: + processed_in_batch += 1 + else: processed_in_batch += 1 - video_segments[out_frame_idx] = { + frame_masks = { out_obj_id: (out_mask_logits[i] > 0.0).squeeze(0).cpu().numpy() for i, out_obj_id in enumerate(out_obj_ids) } + if collect_segments: + video_segments[out_frame_idx] = frame_masks + if frame_callback is not None: + frame_callback(out_frame_idx, frame_masks) last_processed_frame_idx = out_frame_idx if processed_in_batch == 0 or last_processed_frame_idx is None: @@ -323,7 +358,7 @@ def _propagate_in_video_batched( current_start = next_start - return video_segments + return video_segments if collect_segments else {} def propagate_in_video( self, @@ -332,6 +367,8 @@ def propagate_in_video( reverse=False, batch_size=None, online_mode=None, + collect_segments=True, + frame_callback=None, ): use_online_mode = self.online_mode if online_mode is None else bool(online_mode) if use_online_mode: @@ -340,21 +377,27 @@ def propagate_in_video( max_frame_num_to_track=max_frame_num_to_track, reverse=reverse, batch_size=batch_size, + collect_segments=collect_segments, + frame_callback=frame_callback, ) - video_segments = {} + video_segments = {} if collect_segments else None for out_frame_idx, out_obj_ids, out_mask_logits in self.predictor.propagate_in_video( self.inference_state, start_frame_idx, max_frame_num_to_track, reverse, ): - video_segments[out_frame_idx] = { + frame_masks = { out_obj_id: (out_mask_logits[i] > 0.0).squeeze(0).cpu().numpy() for i, out_obj_id in enumerate(out_obj_ids) } + if collect_segments: + video_segments[out_frame_idx] = frame_masks + if frame_callback is not None: + frame_callback(out_frame_idx, frame_masks) - return video_segments + return video_segments if collect_segments else {} def clear_all_prompts_in_frame(self, frame_idx, obj_id): self.predictor.clear_all_prompts_in_frame( @@ -367,4 +410,4 @@ def remove_object(self, obj_id): self.predictor.remove_object( inference_state=self.inference_state, obj_id=obj_id - ) \ No newline at end of file + ) diff --git a/backend/utils.py b/backend/utils.py index 4c9a28f..2f316f4 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -3,6 +3,8 @@ from pathlib import Path import shutil import hashlib +import json +from typing import Any def _color_from_obj_id(obj_id): @@ -301,3 +303,93 @@ def extract_video_to_frames(video_path: Path, output_root: Path, image_extension raise ValueError(f"No frames could be extracted from video: {video_path}") return output_dir + + +def encode_mask_to_rle(mask: np.ndarray) -> list[list[int]]: + """ + Encode a 2D boolean mask into row-major run-length encoding. + + Returns: + - list of [start_index, run_length] for foreground pixels. + """ + mask_array = np.asarray(mask).astype(bool) + if mask_array.ndim != 2: + raise ValueError(f"encode_mask_to_rle expects 2D mask, got shape {mask_array.shape}") + + flat = mask_array.reshape(-1) + if flat.size == 0: + return [] + + rle: list[list[int]] = [] + in_run = False + run_start = 0 + + for idx, value in enumerate(flat): + if value and not in_run: + in_run = True + run_start = idx + elif not value and in_run: + rle.append([int(run_start), int(idx - run_start)]) + in_run = False + + if in_run: + rle.append([int(run_start), int(flat.size - run_start)]) + + return rle + + +def mask_bbox_xywh(mask: np.ndarray) -> list[int]: + """ + Compute [x, y, width, height] bbox for foreground pixels in a 2D mask. + Returns [0, 0, 0, 0] for empty masks. + """ + mask_array = np.asarray(mask).astype(bool) + if mask_array.ndim != 2: + raise ValueError(f"mask_bbox_xywh expects 2D mask, got shape {mask_array.shape}") + + ys, xs = np.nonzero(mask_array) + if ys.size == 0 or xs.size == 0: + return [0, 0, 0, 0] + + x_min = int(xs.min()) + x_max = int(xs.max()) + y_min = int(ys.min()) + y_max = int(ys.max()) + return [x_min, y_min, x_max - x_min + 1, y_max - y_min + 1] + + +def ensure_masks_dir(video_dir: str | Path) -> Path: + video_path = Path(video_dir) + masks_dir = video_path / "masks" + masks_dir.mkdir(parents=True, exist_ok=True) + return masks_dir + + +def build_empty_mask_manifest( + *, + source_video_path: str | None, + resolved_video_frames_dir: str, + num_frames: int, + frame_height: int | None, + frame_width: int | None, +) -> dict[str, Any]: + return { + "version": 1, + "source_video_path": source_video_path, + "resolved_video_frames_dir": resolved_video_frames_dir, + "num_frames": int(num_frames), + "frame_height": int(frame_height) if frame_height is not None else None, + "frame_width": int(frame_width) if frame_width is not None else None, + "frames": {}, + } + + +def write_mask_manifest(manifest_path: Path, manifest: dict[str, Any]) -> None: + manifest_path.parent.mkdir(parents=True, exist_ok=True) + with manifest_path.open("w", encoding="utf-8") as handle: + json.dump(manifest, handle, ensure_ascii=True, separators=(",", ":")) + + +def load_mask_manifest(manifest_path: Path) -> dict[str, Any]: + with manifest_path.open("r", encoding="utf-8") as handle: + return json.load(handle) From c4e1d36707ddd768aaca10a1b20ccca497e5ad9b Mon Sep 17 00:00:00 2001 From: HarenDev Date: Wed, 22 Apr 2026 15:36:38 -0400 Subject: [PATCH 13/30] Sync video masker UI with backend state --- .../src/app/services/backend.service.ts | 70 +- .../video-masker/video-masker.component.css | 48 +- .../video-masker/video-masker.component.html | 95 ++- .../video-masker.component.spec.ts | 105 +++ .../video-masker/video-masker.component.ts | 792 ++++++++++++++---- 5 files changed, 928 insertions(+), 182 deletions(-) create mode 100644 frontend-ng/data-engine/src/app/video-masker/video-masker.component.spec.ts diff --git a/frontend-ng/data-engine/src/app/services/backend.service.ts b/frontend-ng/data-engine/src/app/services/backend.service.ts index c6b358f..9173acc 100644 --- a/frontend-ng/data-engine/src/app/services/backend.service.ts +++ b/frontend-ng/data-engine/src/app/services/backend.service.ts @@ -27,6 +27,7 @@ export interface VideoPropagateRequest { batch_size?: number; online_mode?: boolean; include_masks_in_response?: boolean; + include_saved_mask_paths?: boolean; max_frames_in_response?: number; max_mask_values_in_response?: number; } @@ -38,17 +39,74 @@ export interface VideoAddMaskRequest { } export interface VideoAddPointsResponse { + request_frame_idx: number; + frame_idx: number; + frame_file: string; out_obj_ids: number[]; out_masks: boolean[][][]; // List of masks (which are 2D boolean arrays) + mask_pixel_counts: Record; + mask_shapes: Record; + single_frame_fallback_used?: boolean; + state_epoch: number; +} + +export interface VideoInitStateResponse { + message: string; + num_frames: number; + resolved_video_frames_dir: string; + source_video_path?: string | null; + online_mode: boolean; + batch_size: number; + offload_video_to_cpu: boolean; + offload_state_to_cpu: boolean; + state_epoch: number; } export interface VideoPropagateResponse { video_segments: { [frame_idx: string]: { [obj_id: string]: boolean[][] } }; saved_mask_paths: { [frame_idx: string]: string[] }; + saved_mask_frame_count?: number; video_segments_total_frames?: number; video_segments_returned_frames?: number; video_segments_returned_mask_values?: number; video_segments_truncated?: boolean; + mask_manifest_path?: string; + state_epoch?: number; +} + +export interface VideoMaskObjectData { + size: [number, number]; + rle: number[][]; + bbox: [number, number, number, number]; +} + +export interface VideoMaskDataResponse { + frame_idx: number; + objects: { [obj_id: string]: VideoMaskObjectData }; +} + +export interface TrackPromptPointsRequest { + model_name: 'cotracker3_online' | 'cotracker3_offline'; + add_support_grid?: boolean; +} + +export interface TrackPromptPointMetadata { + point_id: string; + obj_id: number; + source_frame_idx: number; + source_x: number; + source_y: number; +} + +export interface TrackPromptPointsResponse { + message: string; + model_name: string; + num_points: number; + num_frames: number; + tracks: number[][][]; + visibility: boolean[][]; + points: TrackPromptPointMetadata[]; + state_epoch?: number; } @Injectable({ @@ -109,12 +167,12 @@ export class BackendService { initVideoState( dir: string, options?: Omit - ): Observable { + ): Observable { const payload: VideoInitStateRequest = { video_frames_dir: this.normalizePath(dir), ...options }; - return this.http.post(`${this.apiUrl}/video/init_state`, payload); + return this.http.post(`${this.apiUrl}/video/init_state`, payload); } resetVideoState(): Observable { @@ -152,4 +210,12 @@ export class BackendService { getVideoMaskFrameUrl(frameIdx: number): string { return `${this.apiUrl}/video/mask_frame/${frameIdx}`; } + + getVideoMaskData(frameIdx: number): Observable { + return this.http.get(`${this.apiUrl}/video/mask_data/${frameIdx}`); + } + + trackPromptPoints(request: TrackPromptPointsRequest): Observable { + return this.http.post(`${this.apiUrl}/tracking/track_prompt_points`, request); + } } diff --git a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.css b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.css index 7907025..8c39624 100644 --- a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.css +++ b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.css @@ -10,12 +10,14 @@ background-color: #f0f0f0; border-bottom: 1px solid #ccc; display: flex; + flex-wrap: wrap; gap: 10px; align-items: center; } .top-bar input { flex-grow: 1; + min-width: 260px; padding: 5px; } @@ -104,6 +106,50 @@ canvas { gap: 5px; } +.section label { + display: flex; + flex-direction: column; + gap: 4px; + font-size: 0.9rem; + margin-bottom: 8px; +} + +.section select { + padding: 4px 6px; +} + +.debug-section { + font-size: 0.85rem; +} + +.debug-grid { + display: grid; + grid-template-columns: minmax(96px, 1fr) minmax(84px, auto); + gap: 4px 8px; + align-items: start; +} + +.debug-grid span { + color: #555; +} + +.debug-grid strong { + justify-self: end; + text-align: right; + word-break: break-word; +} + +.debug-discard { + margin: 8px 0 0; + padding: 6px 8px; + border-radius: 4px; + background: #fff2f2; + color: #b42318; + font-size: 0.8rem; + line-height: 1.3; + word-break: break-word; +} + .bottom-bar { padding: 10px; background-color: #f0f0f0; @@ -168,4 +214,4 @@ button:disabled { 100% { transform: rotate(360deg); } -} \ No newline at end of file +} diff --git a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.html b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.html index 8e68c33..1c22036 100644 --- a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.html +++ b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.html @@ -1,8 +1,29 @@
- + + + + +
@@ -42,25 +63,69 @@

Tools

-
-

Actions

- - - +
+

Actions

+ + + + + + + +
+ +
+

Sync Debug

+
+ Target frame{{ targetFrameIdx() }} + Displayed frame{{ displayedFrameIdx() }} + Last request frame{{ lastClickRequestFrameIdx() ?? 'n/a' }} + Last response frame{{ lastBackendResponseFrameIdx() ?? 'n/a' }} + Response frame file{{ lastBackendResponseFrameFile() }} + State epoch{{ stateEpoch() }} + Response epoch{{ lastBackendResponseStateEpoch() ?? 'n/a' }} + Last object{{ lastDebugObjectId() ?? 'n/a' }} + Last pixel count{{ lastMaskPixelCount() ?? 'n/a' }} + Fallback used{{ lastFallbackUsed() ? 'yes' : 'no' }} + Last mask source{{ lastMaskSource() }} +
+

{{ lastDiscardReason() }}

+
-
-
-
- Frame: {{ currentFrameIdx() }} / {{ numFrames() - 1 }} - -
-
+
+
+ Target: {{ targetFrameIdx() }} | Displayed: {{ displayedFrameIdx() }} / {{ numFrames() - 1 }} + +
+
Processing...
- \ No newline at end of file + diff --git a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.spec.ts b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.spec.ts new file mode 100644 index 0000000..4c3995c --- /dev/null +++ b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.spec.ts @@ -0,0 +1,105 @@ +import { TestBed } from '@angular/core/testing'; +import { of } from 'rxjs'; +import { vi } from 'vitest'; +import { BackendService, VideoAddPointsResponse } from '../services/backend.service'; +import { VideoMaskerComponent } from './video-masker.component'; + +describe('VideoMaskerComponent sync contract', () => { + let component: VideoMaskerComponent; + let backendMock: { + addNewPointsOrBox: ReturnType; + }; + + const makeResponse = (overrides: Partial): VideoAddPointsResponse => ({ + request_frame_idx: 5, + frame_idx: 5, + frame_file: '00005.jpg', + out_obj_ids: [1], + out_masks: [[[true, false], [false, false]]], + mask_pixel_counts: { 1: 1 }, + mask_shapes: { 1: [2, 2] }, + state_epoch: 3, + ...overrides, + }); + + beforeEach(async () => { + backendMock = { + addNewPointsOrBox: vi.fn(), + }; + + await TestBed.configureTestingModule({ + imports: [VideoMaskerComponent], + providers: [ + { + provide: BackendService, + useValue: { + addNewPointsOrBox: backendMock.addNewPointsOrBox, + }, + }, + ], + }).compileComponents(); + + const fixture = TestBed.createComponent(VideoMaskerComponent); + component = fixture.componentInstance; + component.selectedObjectId.set(1); + component.objects.set([{ id: 1, name: 'Object 1', color: '#ff0000' }]); + component.stateEpoch.set(3); + component.displayedFrameIdx.set(5); + }); + + it('uses displayed frame index in request and stores mask on that frame', async () => { + backendMock.addNewPointsOrBox.mockReturnValue(of(makeResponse({}))); + + await component.addPoint(12, 24, 1, 5); + + expect(backendMock.addNewPointsOrBox).toHaveBeenCalledWith( + expect.objectContaining({ frame_idx: 5 }), + ); + expect(component.masks().get(5)?.get(1)).toEqual([[true, false], [false, false]]); + expect(component.lastDiscardReason()).toBeNull(); + }); + + it('discards mismatched response frame and rolls back optimistic point', async () => { + backendMock.addNewPointsOrBox.mockReturnValue( + of(makeResponse({ request_frame_idx: 5, frame_idx: 4 })), + ); + + await component.addPoint(10, 20, 1, 5); + + expect(component.masks().get(5)?.get(1)).toBeUndefined(); + expect(component.points().get(5)?.get(1)?.length ?? 0).toBe(0); + expect(component.lastDiscardReason()).toContain('frame mismatch'); + }); + + it('discards stale epoch responses and clears live masks', async () => { + const existingMasks = new Map>(); + existingMasks.set(2, new Map([[1, [[true]]]])); + component.masks.set(existingMasks); + component.liveEditedObjectFrames.set(new Map([[2, new Set([1])]])); + + backendMock.addNewPointsOrBox.mockReturnValue(of(makeResponse({ state_epoch: 4 }))); + + await component.addPoint(14, 18, 1, 5); + + expect(component.stateEpoch()).toBe(4); + expect(component.masks().size).toBe(0); + expect(component.liveEditedObjectFrames().size).toBe(0); + expect(component.lastDiscardReason()).toContain('epoch mismatch'); + }); + + it('keeps frame/object marked as live-edited even when returned mask is empty', async () => { + backendMock.addNewPointsOrBox.mockReturnValue( + of( + makeResponse({ + out_masks: [[[false, false], [false, false]]], + mask_pixel_counts: { 1: 0 }, + }), + ), + ); + + await component.addPoint(30, 40, 1, 5); + + expect(component.liveEditedObjectFrames().get(5)?.has(1)).toBe(true); + expect(component.lastMaskPixelCount()).toBe(0); + }); +}); diff --git a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.ts b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.ts index 5d4d64f..264162b 100644 --- a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.ts +++ b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.ts @@ -1,8 +1,13 @@ -import { Component, ElementRef, ViewChild, signal, effect } from '@angular/core'; +import { Component, ElementRef, ViewChild, effect, signal } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { firstValueFrom } from 'rxjs'; -import { BackendService, VideoAddPointsOrBoxRequest } from '../services/backend.service'; +import { + BackendService, + TrackPromptPointMetadata, + VideoAddPointsOrBoxRequest, + VideoMaskObjectData +} from '../services/backend.service'; interface MaskObject { id: number; @@ -16,6 +21,14 @@ interface Point { label: number; // 1 for positive, 0 for negative } +interface TrackedPointSeries extends TrackPromptPointMetadata { + tracks: number[][]; + visibility: boolean[]; +} + +type TrackingOverlayStyle = 'point' | 'short' | 'full'; +type DebugMaskSource = 'live' | 'manifest' | 'none'; + @Component({ selector: 'app-video-masker', standalone: true, @@ -25,40 +38,213 @@ interface Point { }) export class VideoMaskerComponent { @ViewChild('canvas') canvasRef!: ElementRef; + @ViewChild('videoFileInput') videoFileInputRef?: ElementRef; + @ViewChild('framesDirInput') framesDirInputRef?: ElementRef; videoDir = signal(''); isInitialized = signal(false); numFrames = signal(0); - currentFrameIdx = signal(0); + targetFrameIdx = signal(0); + displayedFrameIdx = signal(-1); + stateEpoch = signal(0); objects = signal([]); selectedObjectId = signal(null); - - // Interaction mode interactionMode = signal<'positive' | 'negative'>('positive'); - // State for visualization - // frameIdx -> objId -> mask (boolean[][]) masks = signal>>(new Map()); - - // frameIdx -> objId -> points points = signal>>(new Map()); - useSavedMaskFrames = signal(false); + liveEditedObjectFrames = signal>>(new Map()); + hasManifestMasks = signal(false); + + trackingModel = signal<'cotracker3_online' | 'cotracker3_offline'>('cotracker3_online'); + trackingOverlayStyle = signal('short'); + trackingUseSupportGrid = signal(false); + trackedPoints = signal([]); isLoading = signal(false); + isFrameLoading = signal(false); + isPointRequestInFlight = signal(false); + lastClickRequestFrameIdx = signal(null); + lastBackendResponseFrameIdx = signal(null); + lastBackendResponseFrameFile = signal('n/a'); + lastBackendResponseStateEpoch = signal(null); + lastDebugObjectId = signal(null); + lastMaskPixelCount = signal(null); + lastFallbackUsed = signal(false); + lastMaskSource = signal('none'); + lastDiscardReason = signal(null); + + private frameLoadToken = 0; + private currentBaseImage: HTMLImageElement | null = null; + private currentMaskObjects: { [objId: string]: VideoMaskObjectData } = {}; constructor(private backend: BackendService) { effect(() => { if (this.isInitialized()) { - this.loadFrame(this.currentFrameIdx()); + this.loadFrame(this.targetFrameIdx()); } }); + + effect(() => { + this.trackingOverlayStyle(); + this.trackedPoints(); + if (this.currentBaseImage) { + this.drawCurrentFrame(); + } + }); + } + + private updateStateEpoch(nextEpoch: number | undefined, source: string): void { + if (typeof nextEpoch !== 'number' || !Number.isFinite(nextEpoch)) { + return; + } + const normalizedEpoch = Math.trunc(nextEpoch); + if (normalizedEpoch <= 0) { + return; + } + const previousEpoch = this.stateEpoch(); + if (previousEpoch !== 0 && previousEpoch !== normalizedEpoch) { + this.masks.set(new Map()); + this.liveEditedObjectFrames.set(new Map()); + this.lastDiscardReason.set(`State epoch changed (${previousEpoch} -> ${normalizedEpoch}) during ${source}; cleared live masks.`); + } + this.stateEpoch.set(normalizedEpoch); + } + + private markObjectAsLiveEdited(frameIdx: number, objId: number): void { + const next = new Map(this.liveEditedObjectFrames()); + const existing = next.get(frameIdx); + const nextSet = existing ? new Set(existing) : new Set(); + nextSet.add(objId); + next.set(frameIdx, nextSet); + this.liveEditedObjectFrames.set(next); + } + + private unmarkObjectAsLiveEdited(frameIdx: number, objId: number): void { + const next = new Map(this.liveEditedObjectFrames()); + const existing = next.get(frameIdx); + if (!existing) { + return; + } + const nextSet = new Set(existing); + nextSet.delete(objId); + if (nextSet.size === 0) { + next.delete(frameIdx); + } else { + next.set(frameIdx, nextSet); + } + this.liveEditedObjectFrames.set(next); + } + + private isObjectLiveEdited(frameIdx: number, objId: number): boolean { + const frameSet = this.liveEditedObjectFrames().get(frameIdx); + return Boolean(frameSet?.has(objId)); + } + + private resetDebugState(): void { + this.lastClickRequestFrameIdx.set(null); + this.lastBackendResponseFrameIdx.set(null); + this.lastBackendResponseFrameFile.set('n/a'); + this.lastBackendResponseStateEpoch.set(null); + this.lastDebugObjectId.set(null); + this.lastMaskPixelCount.set(null); + this.lastFallbackUsed.set(false); + this.lastMaskSource.set('none'); + this.lastDiscardReason.set(null); + } + + private getMaskPixelCount(pixelCounts: Record | undefined, objId: number): number | null { + if (!pixelCounts) { + return null; + } + const direct = (pixelCounts as Record)[objId]; + if (typeof direct === 'number' && Number.isFinite(direct)) { + return Math.trunc(direct); + } + const stringLookup = (pixelCounts as unknown as Record)[String(objId)]; + if (typeof stringLookup === 'number' && Number.isFinite(stringLookup)) { + return Math.trunc(stringLookup); + } + return null; + } + + openVideoFilePicker() { + this.videoFileInputRef?.nativeElement.click(); + } + + openFramesDirPicker() { + this.framesDirInputRef?.nativeElement.click(); + } + + onVideoDirChange(value: string) { + this.videoDir.set(value); + } + + onVideoFileSelected(event: Event) { + const input = event.target as HTMLInputElement; + const file = input.files?.[0]; + if (!file) { + return; + } + + const nativePath = this.getNativeFilePath(file); + if (nativePath) { + this.videoDir.set(nativePath); + } else { + this.videoDir.set(file.name); + this.showPathUnavailableMessage('video'); + } + + input.value = ''; + } + + onFramesDirSelected(event: Event) { + const input = event.target as HTMLInputElement; + const file = input.files?.[0]; + if (!file) { + return; + } + + const nativePath = this.getNativeFilePath(file); + if (nativePath) { + this.videoDir.set(this.getParentDirectory(nativePath)); + } else { + this.showPathUnavailableMessage('directory'); + } + + input.value = ''; + } + + private getNativeFilePath(file: File): string | null { + const fileWithPath = file as File & { path?: string }; + if (typeof fileWithPath.path === 'string' && fileWithPath.path.trim()) { + return fileWithPath.path.trim(); + } + return null; + } + + private getParentDirectory(filePath: string): string { + const separatorIndex = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\')); + if (separatorIndex <= 0) { + return filePath; + } + return filePath.slice(0, separatorIndex); + } + + private showPathUnavailableMessage(target: 'video' | 'directory') { + if (target === 'video') { + alert('Selected video file name is available, but this browser does not expose the full local path. Please paste the full video path manually.'); + return; + } + + alert('Selected folder contents are available, but this browser does not expose the full local directory path. Please paste the full frames directory path manually.'); } async initVideo() { const enteredPath = this.videoDir().trim().replace(/^['\"]|['\"]$/g, ''); if (!enteredPath) { - alert('Please enter a valid video frames directory path.'); + alert('Please enter a valid video frames directory path or pick a video file.'); return; } @@ -67,11 +253,18 @@ export class VideoMaskerComponent { try { const res = await firstValueFrom(this.backend.initVideoState(enteredPath)); this.numFrames.set(res.num_frames); - this.isInitialized.set(true); - this.currentFrameIdx.set(0); - this.useSavedMaskFrames.set(false); + this.targetFrameIdx.set(0); + this.displayedFrameIdx.set(-1); + this.hasManifestMasks.set(false); + this.trackedPoints.set([]); + this.masks.set(new Map()); + this.points.set(new Map()); + this.liveEditedObjectFrames.set(new Map()); this.objects.set([{ id: 1, name: 'Object 1', color: this.getRandomColor() }]); this.selectedObjectId.set(1); + this.updateStateEpoch(res.state_epoch, 'video init'); + this.resetDebugState(); + this.isInitialized.set(true); } catch (err: any) { console.error(err); const errorMessage = err?.error?.detail || err?.error?.error || 'Failed to initialize video'; @@ -82,115 +275,310 @@ export class VideoMaskerComponent { } loadFrame(frameIdx: number) { - const ctx = this.canvasRef.nativeElement.getContext('2d'); - if (!ctx) return; + if (!this.canvasRef?.nativeElement) { + return; + } - const img = new Image(); + const token = ++this.frameLoadToken; + this.isFrameLoading.set(true); + const image = new Image(); const frameUrl = this.backend.getVideoFrameUrl(frameIdx); - const maskFrameUrl = this.backend.getVideoMaskFrameUrl(frameIdx); - let triedFallbackToRawFrame = false; + this.currentMaskObjects = {}; - img.onerror = () => { - if (this.useSavedMaskFrames() && !triedFallbackToRawFrame) { - triedFallbackToRawFrame = true; - img.src = frameUrl; + image.onerror = () => { + if (token !== this.frameLoadToken) { + return; } + console.error(`Failed to load frame image: ${frameUrl}`); + this.isFrameLoading.set(false); }; - img.onload = () => { - this.canvasRef.nativeElement.width = img.width; - this.canvasRef.nativeElement.height = img.height; - this.draw(img); + image.onload = async () => { + if (token !== this.frameLoadToken) { + return; + } + + this.currentBaseImage = image; + this.ensureCanvasSize(image.width, image.height); + this.draw(image, frameIdx); + this.displayedFrameIdx.set(frameIdx); + // The displayed frame is now stable on canvas, so allow interaction immediately. + this.isFrameLoading.set(false); + + await this.loadMaskDataForFrame(frameIdx, token); + if (token !== this.frameLoadToken) { + return; + } + this.draw(image, frameIdx); }; - img.src = this.useSavedMaskFrames() ? maskFrameUrl : frameUrl; + image.src = frameUrl; } - draw(img: HTMLImageElement) { + private ensureCanvasSize(width: number, height: number) { + if (!this.canvasRef?.nativeElement) { + return; + } + const canvas = this.canvasRef.nativeElement; + if (canvas.width !== width || canvas.height !== height) { + canvas.width = width; + canvas.height = height; + } + } + + private async loadMaskDataForFrame(frameIdx: number, token: number) { + if (!this.hasManifestMasks()) { + this.currentMaskObjects = {}; + return; + } + + try { + const response = await firstValueFrom(this.backend.getVideoMaskData(frameIdx)); + if (token !== this.frameLoadToken) { + return; + } + + if ((response as any)?.error) { + this.currentMaskObjects = {}; + return; + } + this.currentMaskObjects = response.objects || {}; + } catch (error) { + console.error(error); + this.currentMaskObjects = {}; + } + } + + private drawCurrentFrame() { + if (!this.currentBaseImage || !this.canvasRef?.nativeElement) { + return; + } + const frameIdx = this.displayedFrameIdx(); + if (frameIdx < 0) { + return; + } + this.draw(this.currentBaseImage, frameIdx); + } + + draw(img: HTMLImageElement, frameIdx: number) { const canvas = this.canvasRef.nativeElement; const ctx = canvas.getContext('2d'); if (!ctx) return; + const liveFrameMasks = this.masks().get(frameIdx); + const liveEditedObjectIds = this.liveEditedObjectFrames().get(frameIdx) ?? new Set(); - // Clear canvas ctx.clearRect(0, 0, canvas.width, canvas.height); - - // Draw image ctx.drawImage(img, 0, 0); - // Draw masks - const frameMasks = this.masks().get(this.currentFrameIdx()); - if (frameMasks) { - frameMasks.forEach((mask, objId) => { - const obj = this.objects().find(o => o.id === objId); + if (this.hasManifestMasks() && Object.keys(this.currentMaskObjects).length > 0) { + for (const [objIdStr, maskData] of Object.entries(this.currentMaskObjects)) { + const objId = parseInt(objIdStr, 10); + if (liveEditedObjectIds.has(objId)) { + continue; + } + const obj = this.objects().find((candidate) => candidate.id === objId); + this.drawMaskFromRle(ctx, maskData, obj?.color || '#ff9800'); + } + } + + if (liveFrameMasks) { + liveFrameMasks.forEach((mask, objId) => { + const normalizedMask = this.normalizeMask2d(mask); + if (!normalizedMask || !this.maskHasForeground(normalizedMask)) { + return; + } + const obj = this.objects().find((candidate) => candidate.id === objId); if (obj) { - this.drawMask(ctx, mask, obj.color); + this.drawMask(ctx, normalizedMask, obj.color); } }); } - // Draw points - const framePoints = this.points().get(this.currentFrameIdx()); + const framePoints = this.points().get(frameIdx); if (framePoints) { - framePoints.forEach((points, objId) => { - const obj = this.objects().find(o => o.id === objId); - if (obj) { - points.forEach(p => { - this.drawPoint(ctx, p, obj.color); - }); - } + framePoints.forEach((frameObjPoints) => { + frameObjPoints.forEach((point) => this.drawPoint(ctx, point)); }); } + + const selectedObjectId = this.selectedObjectId(); + let maskSource: DebugMaskSource = 'none'; + if (selectedObjectId !== null) { + const selectedLiveMask = liveFrameMasks?.get(selectedObjectId); + const normalizedLiveMask = selectedLiveMask ? this.normalizeMask2d(selectedLiveMask) : null; + const hasLiveMask = Boolean(normalizedLiveMask && this.maskHasForeground(normalizedLiveMask)); + if (hasLiveMask) { + maskSource = 'live'; + } else if (!this.isObjectLiveEdited(frameIdx, selectedObjectId) && Boolean(this.currentMaskObjects[String(selectedObjectId)])) { + maskSource = 'manifest'; + } + } + this.lastMaskSource.set(maskSource); + + this.drawTrackingOverlay(ctx, frameIdx); } drawMask(ctx: CanvasRenderingContext2D, mask: boolean[][], color: string) { - const width = mask[0].length; - const height = mask.length; - - // Create an ImageData object + const normalizedMask = this.normalizeMask2d(mask); + if (!normalizedMask?.length || !normalizedMask[0]?.length) { + return; + } + const width = normalizedMask[0].length; + const height = normalizedMask.length; const imageData = ctx.createImageData(width, height); const data = imageData.data; - const [r, g, b] = this.hexToRgb(color); for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { - if (mask[y][x]) { - const index = (y * width + x) * 4; - data[index] = r; // R - data[index + 1] = g; // G - data[index + 2] = b; // B - data[index + 3] = 128; // Alpha (0-255) + if (!normalizedMask[y][x]) { + continue; } + const index = (y * width + x) * 4; + data[index] = r; + data[index + 1] = g; + data[index + 2] = b; + data[index + 3] = 120; } } - // Create a temporary canvas to put the image data const tempCanvas = document.createElement('canvas'); tempCanvas.width = width; tempCanvas.height = height; - tempCanvas.getContext('2d')!.putImageData(imageData, 0, 0); + tempCanvas.getContext('2d')?.putImageData(imageData, 0, 0); + ctx.drawImage(tempCanvas, 0, 0, ctx.canvas.width, ctx.canvas.height); + } + + private normalizeMask2d(mask: unknown): boolean[][] | null { + let candidate: any = mask; + while (Array.isArray(candidate) && candidate.length > 0 && Array.isArray(candidate[0]) && Array.isArray(candidate[0][0])) { + candidate = candidate[0]; + } + if (!Array.isArray(candidate) || candidate.length === 0 || !Array.isArray(candidate[0])) { + return null; + } + return candidate as boolean[][]; + } + + private maskHasForeground(mask: boolean[][]): boolean { + for (const row of mask) { + for (const value of row) { + if (value) { + return true; + } + } + } + return false; + } + + private drawMaskFromRle(ctx: CanvasRenderingContext2D, maskData: VideoMaskObjectData, color: string) { + const size = maskData.size; + if (!Array.isArray(size) || size.length !== 2) { + return; + } + const height = Number(size[0]); + const width = Number(size[1]); + if (!Number.isFinite(height) || !Number.isFinite(width) || height <= 0 || width <= 0) { + return; + } - // Draw the temporary canvas onto the main canvas - // We need to scale it if the main canvas size is different from mask size (should be same) + const imageData = ctx.createImageData(width, height); + const data = imageData.data; + const [r, g, b] = this.hexToRgb(color); + + for (const run of maskData.rle || []) { + if (!Array.isArray(run) || run.length !== 2) { + continue; + } + const start = Math.max(0, Number(run[0]) | 0); + const length = Math.max(0, Number(run[1]) | 0); + const end = Math.min(width * height, start + length); + for (let index = start; index < end; index++) { + const pixelOffset = index * 4; + data[pixelOffset] = r; + data[pixelOffset + 1] = g; + data[pixelOffset + 2] = b; + data[pixelOffset + 3] = 120; + } + } + + const tempCanvas = document.createElement('canvas'); + tempCanvas.width = width; + tempCanvas.height = height; + tempCanvas.getContext('2d')?.putImageData(imageData, 0, 0); ctx.drawImage(tempCanvas, 0, 0, ctx.canvas.width, ctx.canvas.height); } - drawPoint(ctx: CanvasRenderingContext2D, point: Point, color: string) { + drawPoint(ctx: CanvasRenderingContext2D, point: Point) { ctx.beginPath(); ctx.arc(point.x, point.y, 5, 0, 2 * Math.PI); - ctx.fillStyle = point.label === 1 ? color : 'red'; // Positive: object color, Negative: red (or maybe white/black?) - // Actually, usually positive is green, negative is red. But here we have multiple objects. - // Let's say positive is object color, negative is black with object color border? - // For simplicity: Positive = Green, Negative = Red. - ctx.fillStyle = point.label === 1 ? '#00FF00' : '#FF0000'; + ctx.fillStyle = point.label === 1 ? '#00ff00' : '#ff0000'; ctx.fill(); ctx.strokeStyle = 'white'; ctx.lineWidth = 2; ctx.stroke(); } + private drawTrackingOverlay(ctx: CanvasRenderingContext2D, frameIdx: number) { + const style = this.trackingOverlayStyle(); + if (!this.trackedPoints().length) { + return; + } + + for (const pointSeries of this.trackedPoints()) { + if (frameIdx < 0 || frameIdx >= pointSeries.tracks.length) { + continue; + } + + const obj = this.objects().find((candidate) => candidate.id === pointSeries.obj_id); + const color = obj?.color || '#ffd54f'; + const visibleNow = pointSeries.visibility[frameIdx] !== false; + + if (style !== 'point') { + const trailStart = style === 'short' + ? Math.max(pointSeries.source_frame_idx, frameIdx - 20) + : Math.max(pointSeries.source_frame_idx, 0); + + ctx.beginPath(); + let started = false; + for (let idx = trailStart; idx <= frameIdx; idx++) { + if (idx < 0 || idx >= pointSeries.tracks.length || pointSeries.visibility[idx] === false) { + continue; + } + const [x, y] = pointSeries.tracks[idx]; + if (!started) { + ctx.moveTo(x, y); + started = true; + } else { + ctx.lineTo(x, y); + } + } + ctx.strokeStyle = color; + ctx.lineWidth = 2; + ctx.globalAlpha = style === 'short' ? 0.75 : 0.55; + ctx.stroke(); + ctx.globalAlpha = 1; + } + + if (!visibleNow) { + continue; + } + + const [currentX, currentY] = pointSeries.tracks[frameIdx]; + ctx.beginPath(); + ctx.arc(currentX, currentY, 4.5, 0, 2 * Math.PI); + ctx.fillStyle = color; + ctx.fill(); + ctx.strokeStyle = '#ffffff'; + ctx.lineWidth = 1.5; + ctx.stroke(); + } + } + onCanvasClick(event: MouseEvent) { - if (!this.isInitialized() || this.selectedObjectId() === null) return; + if (!this.isInitialized() || this.selectedObjectId() === null || this.isFrameLoading() || this.isPointRequestInFlight() || !this.currentBaseImage) { + return; + } const rect = this.canvasRef.nativeElement.getBoundingClientRect(); const scaleX = this.canvasRef.nativeElement.width / rect.width; @@ -198,82 +586,150 @@ export class VideoMaskerComponent { const x = (event.clientX - rect.left) * scaleX; const y = (event.clientY - rect.top) * scaleY; - const label = this.interactionMode() === 'positive' ? 1 : 0; - - this.addPoint(x, y, label); + const frameIdx = this.displayedFrameIdx(); + if (frameIdx < 0) { + return; + } + this.addPoint(x, y, label, frameIdx); } - async addPoint(x: number, y: number, label: number) { + async addPoint(x: number, y: number, label: number, frameIdx: number) { const objId = this.selectedObjectId(); if (objId === null) return; - // Update local points state - const currentPointsMap = this.points(); - let framePointsMap = currentPointsMap.get(this.currentFrameIdx()); + const pointsMap = this.points(); + let framePointsMap = pointsMap.get(frameIdx); if (!framePointsMap) { framePointsMap = new Map(); - currentPointsMap.set(this.currentFrameIdx(), framePointsMap); + pointsMap.set(frameIdx, framePointsMap); } - let objPoints = framePointsMap.get(objId); - if (!objPoints) { - objPoints = []; - framePointsMap.set(objId, objPoints); + let objectPoints = framePointsMap.get(objId); + if (!objectPoints) { + objectPoints = []; + framePointsMap.set(objId, objectPoints); } - objPoints.push({ x, y, label }); - this.points.set(new Map(currentPointsMap)); // Trigger signal update - - // Call backend - // We need to send ALL points for this object on this frame, or just the new one? - // The API `add_new_points_or_box` takes `points` list. - // Usually SAM2 expects all points for the current interaction? - // The API has `clear_old_points=True` by default. - // If we want to accumulate points, we should probably send all of them, or set `clear_old_points=False`. - // Let's try sending just the new point with `clear_old_points=False`. + const wasLiveEditedBeforeRequest = this.isObjectLiveEdited(frameIdx, objId); + objectPoints.push({ x, y, label }); + const pushedPointIndex = objectPoints.length - 1; + this.points.set(new Map(pointsMap)); + this.markObjectAsLiveEdited(frameIdx, objId); + const frameObjectPoints = objectPoints.map((point) => [point.x, point.y]); + const frameObjectLabels = objectPoints.map((point) => point.label); + const requestFrameIdx = frameIdx; + const expectedEpoch = this.stateEpoch(); + this.lastClickRequestFrameIdx.set(requestFrameIdx); + this.lastDebugObjectId.set(objId); + this.lastMaskPixelCount.set(null); + this.lastBackendResponseFrameIdx.set(null); + this.lastBackendResponseFrameFile.set('n/a'); + this.lastBackendResponseStateEpoch.set(null); + this.lastFallbackUsed.set(false); + this.lastDiscardReason.set(null); const request: VideoAddPointsOrBoxRequest = { - frame_idx: this.currentFrameIdx(), + frame_idx: requestFrameIdx, obj_id: objId, - points: [[x, y]], - labels: [label], - clear_old_points: false + points: frameObjectPoints, + labels: frameObjectLabels, + clear_old_points: true }; try { - const res = await firstValueFrom(this.backend.addNewPointsOrBox(request)); - if (res) { - // Update masks - const currentMasksMap = this.masks(); - let frameMasksMap = currentMasksMap.get(this.currentFrameIdx()); - if (!frameMasksMap) { - frameMasksMap = new Map(); - currentMasksMap.set(this.currentFrameIdx(), frameMasksMap); - } - - // res.out_masks is a list of masks corresponding to out_obj_ids - res.out_obj_ids.forEach((id, index) => { - frameMasksMap!.set(id, res.out_masks[index]); - }); + this.isPointRequestInFlight.set(true); + const response = await firstValueFrom(this.backend.addNewPointsOrBox(request)); + if ( + (response as any)?.error || + typeof (response as any)?.request_frame_idx !== 'number' || + typeof (response as any)?.frame_idx !== 'number' || + typeof (response as any)?.frame_file !== 'string' || + typeof (response as any)?.state_epoch !== 'number' || + !Array.isArray((response as any)?.out_obj_ids) || + !Array.isArray((response as any)?.out_masks) || + typeof (response as any)?.mask_pixel_counts !== 'object' + ) { + throw new Error((response as any)?.error || 'Invalid mask response'); + } + const responseStateEpoch = Math.trunc(response.state_epoch); + this.lastBackendResponseStateEpoch.set(responseStateEpoch); + if (responseStateEpoch !== expectedEpoch) { + this.updateStateEpoch(responseStateEpoch, 'add_new_points_or_box mismatch response'); + this.lastDiscardReason.set( + `Discarded stale response due to epoch mismatch (expected ${expectedEpoch}, got ${responseStateEpoch}).` + ); + throw new Error(this.lastDiscardReason() || 'Discarded stale response'); + } + const responseRequestFrameIdx = Math.trunc(response.request_frame_idx); + const responseFrameIdx = Math.trunc(response.frame_idx); + this.lastBackendResponseFrameIdx.set(responseFrameIdx); + this.lastBackendResponseFrameFile.set(response.frame_file || 'n/a'); + if (responseRequestFrameIdx !== requestFrameIdx || responseFrameIdx !== requestFrameIdx) { + this.lastDiscardReason.set( + `Discarded response due to frame mismatch (request=${requestFrameIdx}, response_request=${responseRequestFrameIdx}, response_frame=${responseFrameIdx}).` + ); + throw new Error(this.lastDiscardReason() || 'Discarded mismatched response frame'); + } + if (this.displayedFrameIdx() !== requestFrameIdx) { + this.lastDiscardReason.set( + `Discarded response because displayed frame moved from ${requestFrameIdx} to ${this.displayedFrameIdx()}.` + ); + throw new Error(this.lastDiscardReason() || 'Displayed frame changed during request'); + } - this.masks.set(new Map(currentMasksMap)); + const maskPixelCount = this.getMaskPixelCount(response.mask_pixel_counts, objId); + this.lastMaskPixelCount.set(maskPixelCount); + this.lastFallbackUsed.set(Boolean(response.single_frame_fallback_used)); - // Redraw - // We need to reload the image to clear and redraw everything - this.loadFrame(this.currentFrameIdx()); + const masksMap = this.masks(); + let frameMasksMap = masksMap.get(requestFrameIdx); + if (!frameMasksMap) { + frameMasksMap = new Map(); + masksMap.set(requestFrameIdx, frameMasksMap); } - } catch (err) { - console.error(err); + response.out_obj_ids.forEach((id, index) => { + frameMasksMap?.set(id, response.out_masks[index]); + }); + this.masks.set(new Map(masksMap)); + this.drawCurrentFrame(); + } catch (error) { + console.error(error); + if (objectPoints[pushedPointIndex]) { + objectPoints.splice(pushedPointIndex, 1); + if (objectPoints.length === 0) { + framePointsMap.delete(objId); + } + if (framePointsMap.size === 0) { + pointsMap.delete(frameIdx); + } + if (!wasLiveEditedBeforeRequest && (!objectPoints || objectPoints.length === 0)) { + this.unmarkObjectAsLiveEdited(frameIdx, objId); + } + this.points.set(new Map(pointsMap)); + this.drawCurrentFrame(); + } + } finally { + this.isPointRequestInFlight.set(false); } } + onScrubberFrameChange(value: number | string) { + const parsed = Number(value); + if (!Number.isFinite(parsed)) { + return; + } + const frameIdx = Math.trunc(parsed); + const maxFrame = Math.max(this.numFrames() - 1, 0); + this.targetFrameIdx.set(Math.min(Math.max(frameIdx, 0), maxFrame)); + } + addObject() { const newId = this.objects().length + 1; - const newObj: MaskObject = { + const newObject: MaskObject = { id: newId, name: `Object ${newId}`, color: this.getRandomColor() }; - this.objects.update(objs => [...objs, newObj]); + this.objects.update((existing) => [...existing, newObject]); this.selectedObjectId.set(newId); } @@ -282,69 +738,77 @@ export class VideoMaskerComponent { if (id === null) return; this.backend.removeObject(id).subscribe(() => { - this.objects.update(objs => objs.filter(o => o.id !== id)); - if (this.objects().length > 0) { - this.selectedObjectId.set(this.objects()[0].id); - } else { - this.selectedObjectId.set(null); - } - // Also clear masks and points for this object - // ... implementation omitted for brevity, but should be done + this.objects.update((existing) => existing.filter((entry) => entry.id !== id)); + this.removeObjectFromFrameMaps(id); + this.selectedObjectId.set(this.objects().length > 0 ? this.objects()[0].id : null); + this.drawCurrentFrame(); }); } + private removeObjectFromFrameMaps(objectId: number) { + const nextMasks = new Map(this.masks()); + nextMasks.forEach((frameMap) => frameMap.delete(objectId)); + this.masks.set(nextMasks); + + const nextPoints = new Map(this.points()); + nextPoints.forEach((frameMap) => frameMap.delete(objectId)); + this.points.set(nextPoints); + + this.trackedPoints.set(this.trackedPoints().filter((series) => series.obj_id !== objectId)); + } + async propagate() { this.isLoading.set(true); try { - const res = await firstValueFrom(this.backend.propagateInVideo({ - include_masks_in_response: false + const response = await firstValueFrom(this.backend.propagateInVideo({ + include_masks_in_response: false, + include_saved_mask_paths: false })); - if (res && res.video_segments) { - // Update all masks - const currentMasksMap = this.masks(); - const entries = Object.entries(res.video_segments); - - for (const [frameIdxStr, objMasks] of entries) { - const frameIdx = parseInt(frameIdxStr); - let frameMasksMap = currentMasksMap.get(frameIdx); - if (!frameMasksMap) { - frameMasksMap = new Map(); - currentMasksMap.set(frameIdx, frameMasksMap); - } - - for (const [objIdStr, mask] of Object.entries(objMasks)) { - const objId = parseInt(objIdStr); - frameMasksMap.set(objId, mask as boolean[][]); - } - } - - const hasSavedMaskFrames = Object.keys(res.saved_mask_paths || {}).length > 0; - const useSavedFrames = hasSavedMaskFrames && entries.length === 0; - this.useSavedMaskFrames.set(useSavedFrames); + this.updateStateEpoch(response.state_epoch, 'propagation'); + this.hasManifestMasks.set(Boolean(response.mask_manifest_path)); + this.loadFrame(this.targetFrameIdx()); + } catch (error) { + console.error(error); + alert('Propagation failed'); + } finally { + this.isLoading.set(false); + } + } - if (useSavedFrames) { - this.masks.set(new Map()); - } else { - this.masks.set(new Map(currentMasksMap)); - } + async runTracking() { + this.isLoading.set(true); + try { + const response = await firstValueFrom(this.backend.trackPromptPoints({ + model_name: this.trackingModel(), + add_support_grid: this.trackingUseSupportGrid() + })); + this.updateStateEpoch(response.state_epoch, 'tracking restore'); - this.loadFrame(this.currentFrameIdx()); // Redraw current frame - } - } catch (err) { - console.error(err); - alert('Propagation failed'); + const trackedSeries: TrackedPointSeries[] = response.points.map((point, index) => ({ + ...point, + tracks: response.tracks[index] || [], + visibility: response.visibility[index] || [] + })); + this.trackedPoints.set(trackedSeries); + this.drawCurrentFrame(); + } catch (error: any) { + console.error(error); + const errorMessage = error?.error?.detail || 'Tracking failed'; + alert(errorMessage); } finally { this.isLoading.set(false); } } clearMasks() { - // This should probably call reset_state on backend - this.backend.resetVideoState().subscribe(() => { - this.useSavedMaskFrames.set(false); + this.backend.resetVideoState().subscribe((response) => { + this.updateStateEpoch(response?.state_epoch, 'reset'); + this.hasManifestMasks.set(false); + this.trackedPoints.set([]); this.masks.set(new Map()); this.points.set(new Map()); - this.loadFrame(this.currentFrameIdx()); + this.liveEditedObjectFrames.set(new Map()); + this.loadFrame(this.targetFrameIdx()); }); } From 2deb4bba4cf849003e38f95cae546827decda5ec Mon Sep 17 00:00:00 2001 From: "Rafael A." <157764758+HarenDev@users.noreply.github.com> Date: Mon, 27 Apr 2026 21:13:55 -0400 Subject: [PATCH 14/30] First pass on adding Tauri implementation --- README.md | 22 +- frontend-ng/data-engine/.gitignore | 1 + frontend-ng/data-engine/README.md | 28 + frontend-ng/data-engine/angular.json | 1 + frontend-ng/data-engine/bun.lock | 1448 +++++++++++++++++ frontend-ng/data-engine/package.json | 12 +- frontend-ng/data-engine/src-tauri/Cargo.toml | 18 + frontend-ng/data-engine/src-tauri/build.rs | 3 + .../src-tauri/capabilities/default.json | 12 + frontend-ng/data-engine/src-tauri/src/lib.rs | 7 + frontend-ng/data-engine/src-tauri/src/main.rs | 3 + .../data-engine/src-tauri/tauri.conf.json | 29 + frontend-ng/data-engine/src/app/app.spec.ts | 4 +- .../src/app/services/backend.service.spec.ts | 66 + .../src/app/services/backend.service.ts | 82 +- .../services/desktop-bridge.service.spec.ts | 21 + .../app/services/desktop-bridge.service.ts | 75 + .../video-masker/video-masker.component.css | 22 + .../video-masker/video-masker.component.html | 16 +- .../video-masker.component.spec.ts | 42 + .../video-masker/video-masker.component.ts | 47 +- sam2 | 1 + 22 files changed, 1931 insertions(+), 29 deletions(-) create mode 100644 frontend-ng/data-engine/bun.lock create mode 100644 frontend-ng/data-engine/src-tauri/Cargo.toml create mode 100644 frontend-ng/data-engine/src-tauri/build.rs create mode 100644 frontend-ng/data-engine/src-tauri/capabilities/default.json create mode 100644 frontend-ng/data-engine/src-tauri/src/lib.rs create mode 100644 frontend-ng/data-engine/src-tauri/src/main.rs create mode 100644 frontend-ng/data-engine/src-tauri/tauri.conf.json create mode 100644 frontend-ng/data-engine/src/app/services/backend.service.spec.ts create mode 100644 frontend-ng/data-engine/src/app/services/desktop-bridge.service.spec.ts create mode 100644 frontend-ng/data-engine/src/app/services/desktop-bridge.service.ts create mode 160000 sam2 diff --git a/README.md b/README.md index 9e3b22d..70ec3a4 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,26 @@ A complete data annotation and tracking system with both Python (FastAPI) backen 1. Run `fastapi dev backend/api.py` to test the API manually with hot reload, or run `python3 backend/tests/tester.py` to test the API automatically and see results under `backend/tests/` +## Angular Web Frontend + +1. Change into `frontend-ng/data-engine` +2. Install dependencies with `bun install` +3. Start the frontend with `bun run start` +4. Keep the Python backend running separately + +The frontend defaults to `http://127.0.0.1:8000` for backend requests and also exposes a compact API URL override in the top bar. + +## Tauri Desktop Frontend + +1. Change into `frontend-ng/data-engine` +2. Install JavaScript dependencies with `bun install` +3. Install the Rust toolchain and Tauri prerequisites for your OS +4. Start the Python backend separately +5. Run `bun run tauri:dev` for the desktop dev workflow +6. Run `bun run tauri:build` to produce a packaged desktop build + +The Tauri app only wraps the Angular frontend. It does not bundle or launch the Python backend. + ## Testing via C++ 1. Install `nlohmann_json` and `curl` dev packages via your package manager @@ -28,4 +48,4 @@ A complete data annotation and tracking system with both Python (FastAPI) backen 4. Run `cmake --build build` to compile project -5. Run `./build/src/BackendInterfaceTests` while the venv is sourced to test backend code \ No newline at end of file +5. Run `./build/src/BackendInterfaceTests` while the venv is sourced to test backend code diff --git a/frontend-ng/data-engine/.gitignore b/frontend-ng/data-engine/.gitignore index b1d225e..3a59b1c 100644 --- a/frontend-ng/data-engine/.gitignore +++ b/frontend-ng/data-engine/.gitignore @@ -37,6 +37,7 @@ yarn-error.log testem.log /typings __screenshots__/ +/src-tauri/target # System files .DS_Store diff --git a/frontend-ng/data-engine/README.md b/frontend-ng/data-engine/README.md index 670cc7c..d7c2e87 100644 --- a/frontend-ng/data-engine/README.md +++ b/frontend-ng/data-engine/README.md @@ -12,6 +12,34 @@ ng serve Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files. +The frontend expects the FastAPI backend to be running separately and defaults to `http://127.0.0.1:8000`. You can override that URL from the compact API control in the top bar. + +## Desktop Tauri workflow + +The same Angular app can also run as a Tauri desktop application. + +1. Install Bun dependencies: + +```bash +bun install +``` + +2. Start the backend separately. + +3. Start the desktop app in development: + +```bash +bun run tauri:dev +``` + +4. Build a packaged desktop app: + +```bash +bun run tauri:build +``` + +The desktop build adds native file and directory pickers, but it still talks to the same HTTP backend API. + ## Code scaffolding Angular CLI includes powerful code scaffolding tools. To generate a new component, run: diff --git a/frontend-ng/data-engine/angular.json b/frontend-ng/data-engine/angular.json index 79aa8af..30a2f80 100644 --- a/frontend-ng/data-engine/angular.json +++ b/frontend-ng/data-engine/angular.json @@ -17,6 +17,7 @@ "builder": "@angular/build:application", "options": { "browser": "src/main.ts", + "outputPath": "dist/data-engine", "tsConfig": "tsconfig.app.json", "assets": [ { diff --git a/frontend-ng/data-engine/bun.lock b/frontend-ng/data-engine/bun.lock new file mode 100644 index 0000000..ea0fe0f --- /dev/null +++ b/frontend-ng/data-engine/bun.lock @@ -0,0 +1,1448 @@ +{ + "lockfileVersion": 1, + "configVersion": 0, + "workspaces": { + "": { + "name": "data-engine", + "dependencies": { + "@angular/common": "^21.0.0", + "@angular/compiler": "^21.0.0", + "@angular/core": "^21.0.0", + "@angular/forms": "^21.0.0", + "@angular/platform-browser": "^21.0.0", + "@angular/router": "^21.0.0", + "@tauri-apps/api": "^2.0.0", + "@tauri-apps/plugin-dialog": "^2.0.0", + "rxjs": "~7.8.0", + "tslib": "^2.3.0", + }, + "devDependencies": { + "@angular/build": "^21.0.0", + "@angular/cli": "^21.0.0", + "@angular/compiler-cli": "^21.0.0", + "@tailwindcss/postcss": "^4.1.12", + "@tauri-apps/cli": "^2.0.0", + "jsdom": "^27.1.0", + "postcss": "^8.5.3", + "tailwindcss": "^4.1.12", + "typescript": "~5.9.2", + "vitest": "^4.0.8", + }, + }, + }, + "packages": { + "@acemir/cssom": ["@acemir/cssom@0.9.23", "", {}, "sha512-2kJ1HxBKzPLbmhZpxBiTZggjtgCwKg1ma5RHShxvd6zgqhDEdEkzpiwe7jLkI2p2BrZvFCXIihdoMkl1H39VnA=="], + + "@algolia/abtesting": ["@algolia/abtesting@1.6.1", "", { "dependencies": { "@algolia/client-common": "5.40.1", "@algolia/requester-browser-xhr": "5.40.1", "@algolia/requester-fetch": "5.40.1", "@algolia/requester-node-http": "5.40.1" } }, "sha512-wV/gNRkzb7sI9vs1OneG129hwe3Q5zPj7zigz3Ps7M5Lpo2hSorrOnXNodHEOV+yXE/ks4Pd+G3CDFIjFTWhMQ=="], + + "@algolia/client-abtesting": ["@algolia/client-abtesting@5.40.1", "", { "dependencies": { "@algolia/client-common": "5.40.1", "@algolia/requester-browser-xhr": "5.40.1", "@algolia/requester-fetch": "5.40.1", "@algolia/requester-node-http": "5.40.1" } }, "sha512-cxKNATPY5t+Mv8XAVTI57altkaPH+DZi4uMrnexPxPHODMljhGYY+GDZyHwv9a+8CbZHcY372OkxXrDMZA4Lnw=="], + + "@algolia/client-analytics": ["@algolia/client-analytics@5.40.1", "", { "dependencies": { "@algolia/client-common": "5.40.1", "@algolia/requester-browser-xhr": "5.40.1", "@algolia/requester-fetch": "5.40.1", "@algolia/requester-node-http": "5.40.1" } }, "sha512-XP008aMffJCRGAY8/70t+hyEyvqqV7YKm502VPu0+Ji30oefrTn2al7LXkITz7CK6I4eYXWRhN6NaIUi65F1OA=="], + + "@algolia/client-common": ["@algolia/client-common@5.40.1", "", {}, "sha512-gWfQuQUBtzUboJv/apVGZMoxSaB0M4Imwl1c9Ap+HpCW7V0KhjBddqF2QQt5tJZCOFsfNIgBbZDGsEPaeKUosw=="], + + "@algolia/client-insights": ["@algolia/client-insights@5.40.1", "", { "dependencies": { "@algolia/client-common": "5.40.1", "@algolia/requester-browser-xhr": "5.40.1", "@algolia/requester-fetch": "5.40.1", "@algolia/requester-node-http": "5.40.1" } }, "sha512-RTLjST/t+lsLMouQ4zeLJq2Ss+UNkLGyNVu+yWHanx6kQ3LT5jv8UvPwyht9s7R6jCPnlSI77WnL80J32ZuyJg=="], + + "@algolia/client-personalization": ["@algolia/client-personalization@5.40.1", "", { "dependencies": { "@algolia/client-common": "5.40.1", "@algolia/requester-browser-xhr": "5.40.1", "@algolia/requester-fetch": "5.40.1", "@algolia/requester-node-http": "5.40.1" } }, "sha512-2FEK6bUomBzEYkTKzD0iRs7Ljtjb45rKK/VSkyHqeJnG+77qx557IeSO0qVFE3SfzapNcoytTofnZum0BQ6r3Q=="], + + "@algolia/client-query-suggestions": ["@algolia/client-query-suggestions@5.40.1", "", { "dependencies": { "@algolia/client-common": "5.40.1", "@algolia/requester-browser-xhr": "5.40.1", "@algolia/requester-fetch": "5.40.1", "@algolia/requester-node-http": "5.40.1" } }, "sha512-Nju4NtxAvXjrV2hHZNLKVJLXjOlW6jAXHef/CwNzk1b2qIrCWDO589ELi5ZHH1uiWYoYyBXDQTtHmhaOVVoyXg=="], + + "@algolia/client-search": ["@algolia/client-search@5.40.1", "", { "dependencies": { "@algolia/client-common": "5.40.1", "@algolia/requester-browser-xhr": "5.40.1", "@algolia/requester-fetch": "5.40.1", "@algolia/requester-node-http": "5.40.1" } }, "sha512-Mw6pAUF121MfngQtcUb5quZVqMC68pSYYjCRZkSITC085S3zdk+h/g7i6FxnVdbSU6OztxikSDMh1r7Z+4iPlA=="], + + "@algolia/ingestion": ["@algolia/ingestion@1.40.1", "", { "dependencies": { "@algolia/client-common": "5.40.1", "@algolia/requester-browser-xhr": "5.40.1", "@algolia/requester-fetch": "5.40.1", "@algolia/requester-node-http": "5.40.1" } }, "sha512-z+BPlhs45VURKJIxsR99NNBWpUEEqIgwt10v/fATlNxc4UlXvALdOsWzaFfe89/lbP5Bu4+mbO59nqBC87ZM/g=="], + + "@algolia/monitoring": ["@algolia/monitoring@1.40.1", "", { "dependencies": { "@algolia/client-common": "5.40.1", "@algolia/requester-browser-xhr": "5.40.1", "@algolia/requester-fetch": "5.40.1", "@algolia/requester-node-http": "5.40.1" } }, "sha512-VJMUMbO0wD8Rd2VVV/nlFtLJsOAQvjnVNGkMkspFiFhpBA7s/xJOb+fJvvqwKFUjbKTUA7DjiSi1ljSMYBasXg=="], + + "@algolia/recommend": ["@algolia/recommend@5.40.1", "", { "dependencies": { "@algolia/client-common": "5.40.1", "@algolia/requester-browser-xhr": "5.40.1", "@algolia/requester-fetch": "5.40.1", "@algolia/requester-node-http": "5.40.1" } }, "sha512-ehvJLadKVwTp9Scg9NfzVSlBKH34KoWOQNTaN8i1Ac64AnO6iH2apJVSP6GOxssaghZ/s8mFQsDH3QIZoluFHA=="], + + "@algolia/requester-browser-xhr": ["@algolia/requester-browser-xhr@5.40.1", "", { "dependencies": { "@algolia/client-common": "5.40.1" } }, "sha512-PbidVsPurUSQIr6X9/7s34mgOMdJnn0i6p+N6Ab+lsNhY5eiu+S33kZEpZwkITYBCIbhzDLOvb7xZD3gDi+USA=="], + + "@algolia/requester-fetch": ["@algolia/requester-fetch@5.40.1", "", { "dependencies": { "@algolia/client-common": "5.40.1" } }, "sha512-ThZ5j6uOZCF11fMw9IBkhigjOYdXGXQpj6h4k+T9UkZrF2RlKcPynFzDeRgaLdpYk8Yn3/MnFbwUmib7yxj5Lw=="], + + "@algolia/requester-node-http": ["@algolia/requester-node-http@5.40.1", "", { "dependencies": { "@algolia/client-common": "5.40.1" } }, "sha512-H1gYPojO6krWHnUXu/T44DrEun/Wl95PJzMXRcM/szstNQczSbwq6wIFJPI9nyE95tarZfUNU3rgorT+wZ6iCQ=="], + + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + + "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], + + "@angular-devkit/architect": ["@angular-devkit/architect@0.2100.0", "", { "dependencies": { "@angular-devkit/core": "21.0.0", "rxjs": "7.8.2" } }, "sha512-BNt6Rw53WauCw31ku/r/ksVIY+Pi8XZptsSUIHiDUeqB2iZOWu4L3c5kuDGmoGkGByY588H48hfR2MgIpBhgAg=="], + + "@angular-devkit/core": ["@angular-devkit/core@21.0.0", "", { "dependencies": { "ajv": "8.17.1", "ajv-formats": "3.0.1", "jsonc-parser": "3.3.1", "picomatch": "4.0.3", "rxjs": "7.8.2", "source-map": "0.7.6" }, "peerDependencies": { "chokidar": "^4.0.0" } }, "sha512-d3n5GvrwqN1AUkWE3Wd8rrdY2u6/5bzorlZVT5W4CcH7ekAIoMu4SBTbSJ7bfRe/l2z/A1WZ6hFlnQzLclOjJA=="], + + "@angular-devkit/schematics": ["@angular-devkit/schematics@21.0.0", "", { "dependencies": { "@angular-devkit/core": "21.0.0", "jsonc-parser": "3.3.1", "magic-string": "0.30.19", "ora": "9.0.0", "rxjs": "7.8.2" } }, "sha512-8zwXp8OTzJO3IY3Ge3lLqXokNAtQy6kM1FeTyPT20M+0AQHTX9WJlGaYEWdLYI9WwNPWy1/Iq6AaZNcR5phPpw=="], + + "@angular/build": ["@angular/build@21.0.0", "", { "dependencies": { "@ampproject/remapping": "2.3.0", "@angular-devkit/architect": "0.2100.0", "@babel/core": "7.28.4", "@babel/helper-annotate-as-pure": "7.27.3", "@babel/helper-split-export-declaration": "7.24.7", "@inquirer/confirm": "5.1.19", "@vitejs/plugin-basic-ssl": "2.1.0", "beasties": "0.3.5", "browserslist": "^4.26.0", "esbuild": "0.26.0", "https-proxy-agent": "7.0.6", "istanbul-lib-instrument": "6.0.3", "jsonc-parser": "3.3.1", "listr2": "9.0.5", "magic-string": "0.30.19", "mrmime": "2.0.1", "parse5-html-rewriting-stream": "8.0.0", "picomatch": "4.0.3", "piscina": "5.1.3", "rolldown": "1.0.0-beta.47", "sass": "1.93.2", "semver": "7.7.3", "source-map-support": "0.5.21", "tinyglobby": "0.2.15", "undici": "7.16.0", "vite": "7.2.2", "watchpack": "2.4.4" }, "optionalDependencies": { "lmdb": "3.4.3" }, "peerDependencies": { "@angular/compiler": "^21.0.0", "@angular/compiler-cli": "^21.0.0", "@angular/core": "^21.0.0", "@angular/localize": "^21.0.0", "@angular/platform-browser": "^21.0.0", "@angular/platform-server": "^21.0.0", "@angular/service-worker": "^21.0.0", "@angular/ssr": "^21.0.0", "karma": "^6.4.0", "less": "^4.2.0", "ng-packagr": "^21.0.0", "postcss": "^8.4.0", "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", "tslib": "^2.3.0", "typescript": ">=5.9 <6.0", "vitest": "^4.0.8" }, "optionalPeers": ["@angular/localize", "@angular/platform-server", "@angular/service-worker", "@angular/ssr", "karma", "less", "ng-packagr"] }, "sha512-TobXT9fXZVee1yULlcOVowOurCUoJlku8st5vzkRZekP520qRjBSEbIk8V2emkFbzgzOeJUtXv1pvrBY7yAYhQ=="], + + "@angular/cli": ["@angular/cli@21.0.0", "", { "dependencies": { "@angular-devkit/architect": "0.2100.0", "@angular-devkit/core": "21.0.0", "@angular-devkit/schematics": "21.0.0", "@inquirer/prompts": "7.9.0", "@listr2/prompt-adapter-inquirer": "3.0.5", "@modelcontextprotocol/sdk": "1.20.1", "@schematics/angular": "21.0.0", "@yarnpkg/lockfile": "1.1.0", "algoliasearch": "5.40.1", "ini": "5.0.0", "jsonc-parser": "3.3.1", "listr2": "9.0.5", "npm-package-arg": "13.0.1", "pacote": "21.0.3", "parse5-html-rewriting-stream": "8.0.0", "resolve": "1.22.11", "semver": "7.7.3", "yargs": "18.0.0", "zod": "3.25.76" }, "bin": { "ng": "bin/ng.js" } }, "sha512-713DfTD/ThIy/BOmZ+8zhXo/OhPE9jYaAS0UhXVhtp2ptqzRqSzLvW9fWgtqP4ITAqulOoitiWPLXxOEQ2Cixw=="], + + "@angular/common": ["@angular/common@21.0.0", "", { "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/core": "21.0.0", "rxjs": "^6.5.3 || ^7.4.0" } }, "sha512-uFvQDYU5X5nEnI9C4Bkdxcu4aIzNesGLJzmFlnwChVxB4BxIRF0uHL0oRhdkInGTIzPDJPH4nF6B/22c5gDVqA=="], + + "@angular/compiler": ["@angular/compiler@21.0.0", "", { "dependencies": { "tslib": "^2.3.0" } }, "sha512-6jCH3UYga5iokj5F40SR4dlwo9ZRMkT8YzHCTijwZuDX9zvugp9jPof092RvIeNsTvCMVfGWuM9yZ1DRUsU/yg=="], + + "@angular/compiler-cli": ["@angular/compiler-cli@21.0.0", "", { "dependencies": { "@babel/core": "7.28.4", "@jridgewell/sourcemap-codec": "^1.4.14", "chokidar": "^4.0.0", "convert-source-map": "^1.5.1", "reflect-metadata": "^0.2.0", "semver": "^7.0.0", "tslib": "^2.3.0", "yargs": "^18.0.0" }, "peerDependencies": { "@angular/compiler": "21.0.0", "typescript": ">=5.9 <6.0" }, "bin": { "ng-xi18n": "bundles/src/bin/ng_xi18n.js", "ngc": "bundles/src/bin/ngc.js" } }, "sha512-KTXp+e2UPGyfFew6Wq95ULpHWQ20dhqkAMZ6x6MCYfOe2ccdnGYsAbLLmnWGmSg5BaOI4B0x/1XCFZf/n6WDgA=="], + + "@angular/core": ["@angular/core@21.0.0", "", { "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/compiler": "21.0.0", "rxjs": "^6.5.3 || ^7.4.0", "zone.js": "~0.15.0" }, "optionalPeers": ["zone.js"] }, "sha512-bqi8fT4csyITeX8vdN5FJDBWx5wuWzdCg4mKSjHd+onVzZLyZ8bcnuAKz4mklgvjvwuXoRYukmclUurLwfq3Rg=="], + + "@angular/forms": ["@angular/forms@21.0.0", "", { "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/common": "21.0.0", "@angular/core": "21.0.0", "@angular/platform-browser": "21.0.0", "@standard-schema/spec": "^1.0.0", "rxjs": "^6.5.3 || ^7.4.0" } }, "sha512-kcudwbZs/ddKqaELz4eEW9kOGCsX61qsf9jkQsGTARBEOUcU2K+rM6mX5sTf9azHvQ9wlX4N36h0eYzBA4Y4Qg=="], + + "@angular/platform-browser": ["@angular/platform-browser@21.0.0", "", { "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/animations": "21.0.0", "@angular/common": "21.0.0", "@angular/core": "21.0.0" }, "optionalPeers": ["@angular/animations"] }, "sha512-KQrANla4RBLhcGkwlndqsKzBwVFOWQr1640CfBVjj2oz4M3dW5hyMtXivBACvuwyUhYU/qJbqlDMBXl/OUSudQ=="], + + "@angular/router": ["@angular/router@21.0.0", "", { "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/common": "21.0.0", "@angular/core": "21.0.0", "@angular/platform-browser": "21.0.0", "rxjs": "^6.5.3 || ^7.4.0" } }, "sha512-ARx1R2CmTgAezlMkUpV40V4T/IbXhL7dm4SuMVKbuEOsCKZC0TLOSSTsGYY7HKem45JHlJaByv819cJnabFgBg=="], + + "@asamuzakjp/css-color": ["@asamuzakjp/css-color@4.1.0", "", { "dependencies": { "@csstools/css-calc": "^2.1.4", "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "lru-cache": "^11.2.2" } }, "sha512-9xiBAtLn4aNsa4mDnpovJvBn72tNEIACyvlqaNJ+ADemR+yeMJWnBudOi2qGDviJa7SwcDOU/TRh5dnET7qk0w=="], + + "@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@6.7.4", "", { "dependencies": { "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.1.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.2.2" } }, "sha512-buQDjkm+wDPXd6c13534URWZqbz0RP5PAhXZ+LIoa5LgwInT9HVJvGIJivg75vi8I13CxDGdTnz+aY5YUJlIAA=="], + + "@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="], + + "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="], + + "@babel/core": ["@babel/core@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA=="], + + "@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], + + "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="], + + "@babel/helper-split-export-declaration": ["@babel/helper-split-export-declaration@7.24.7", "", { "dependencies": { "@babel/types": "^7.24.7" } }, "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + + "@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="], + + "@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + + "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + + "@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], + + "@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + + "@csstools/color-helpers": ["@csstools/color-helpers@5.1.0", "", {}, "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA=="], + + "@csstools/css-calc": ["@csstools/css-calc@2.1.4", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ=="], + + "@csstools/css-color-parser": ["@csstools/css-color-parser@3.1.0", "", { "dependencies": { "@csstools/color-helpers": "^5.1.0", "@csstools/css-calc": "^2.1.4" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA=="], + + "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@3.0.5", "", { "peerDependencies": { "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ=="], + + "@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.0.16", "", {}, "sha512-2SpS4/UaWQaGpBINyG5ZuCHnUDeVByOhvbkARwfmnfxDvTaj80yOI1cD8Tw93ICV5Fx4fnyDKWQZI1CDtcWyUg=="], + + "@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="], + + "@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.26.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-hj0sKNCQOOo2fgyII3clmJXP28VhgDfU5iy3GNHlWO76KG6N7x4D9ezH5lJtQTG+1J6MFDAJXC1qsI+W+LvZoA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.26.0", "", { "os": "android", "cpu": "arm" }, "sha512-C0hkDsYNHZkBtPxxDx177JN90/1MiCpvBNjz1f5yWJo1+5+c5zr8apjastpEG+wtPjo9FFtGG7owSsAxyKiHxA=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.26.0", "", { "os": "android", "cpu": "arm64" }, "sha512-DDnoJ5eoa13L8zPh87PUlRd/IyFaIKOlRbxiwcSbeumcJ7UZKdtuMCHa1Q27LWQggug6W4m28i4/O2qiQQ5NZQ=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.26.0", "", { "os": "android", "cpu": "x64" }, "sha512-bKDkGXGZnj0T70cRpgmv549x38Vr2O3UWLbjT2qmIkdIWcmlg8yebcFWoT9Dku7b5OV3UqPEuNKRzlNhjwUJ9A=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.26.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-6Z3naJgOuAIB0RLlJkYc81An3rTlQ/IeRdrU3dOea8h/PvZSgitZV+thNuIccw0MuK1GmIAnAmd5TrMZad8FTQ=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.26.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-OPnYj0zpYW0tHusMefyaMvNYQX5pNQuSsHFTHUBNp3vVXupwqpxofcjVsUx11CQhGVkGeXjC3WLjh91hgBG2xw=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.26.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-jix2fa6GQeZhO1sCKNaNMjfj5hbOvoL2F5t+w6gEPxALumkpOV/wq7oUBMHBn2hY2dOm+mEV/K+xfZy3mrsxNQ=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.26.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tccJaH5xHJD/239LjbVvJwf6T4kSzbk6wPFerF0uwWlkw/u7HL+wnAzAH5GB2irGhYemDgiNTp8wJzhAHQ64oA=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.26.0", "", { "os": "linux", "cpu": "arm" }, "sha512-JY8NyU31SyRmRpuc5W8PQarAx4TvuYbyxbPIpHAZdr/0g4iBr8KwQBS4kiiamGl2f42BBecHusYCsyxi7Kn8UQ=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.26.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-IMJYN7FSkLttYyTbsbme0Ra14cBO5z47kpamo16IwggzzATFY2lcZAwkbcNkWiAduKrTgFJP7fW5cBI7FzcuNQ=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.26.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-XITaGqGVLgk8WOHw8We9Z1L0lbLFip8LyQzKYFKO4zFo1PFaaSKsbNjvkb7O8kEXytmSGRkYpE8LLVpPJpsSlw=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.26.0", "", { "os": "linux", "cpu": "none" }, "sha512-MkggfbDIczStUJwq9wU7gQ7kO33d8j9lWuOCDifN9t47+PeI+9m2QVh51EI/zZQ1spZtFMC1nzBJ+qNGCjJnsg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.26.0", "", { "os": "linux", "cpu": "none" }, "sha512-fUYup12HZWAeccNLhQ5HwNBPr4zXCPgUWzEq2Rfw7UwqwfQrFZ0SR/JljaURR8xIh9t+o1lNUFTECUTmaP7yKA=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.26.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-MzRKhM0Ip+//VYwC8tialCiwUQ4G65WfALtJEFyU0GKJzfTYoPBw5XNWf0SLbCUYQbxTKamlVwPmcw4DgZzFxg=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.26.0", "", { "os": "linux", "cpu": "none" }, "sha512-QhCc32CwI1I4Jrg1enCv292sm3YJprW8WHHlyxJhae/dVs+KRWkbvz2Nynl5HmZDW/m9ZxrXayHzjzVNvQMGQA=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.26.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-1D6vi6lfI18aNT1aTf2HV+RIlm6fxtlAp8eOJ4mmnbYmZ4boz8zYDar86sIYNh0wmiLJEbW/EocaKAX6Yso2fw=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.26.0", "", { "os": "linux", "cpu": "x64" }, "sha512-rnDcepj7LjrKFvZkx+WrBv6wECeYACcFjdNPvVPojCPJD8nHpb3pv3AuR9CXgdnjH1O23btICj0rsp0L9wAnHA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.26.0", "", { "os": "none", "cpu": "arm64" }, "sha512-FSWmgGp0mDNjEXXFcsf12BmVrb+sZBBBlyh3LwB/B9ac3Kkc8x5D2WimYW9N7SUkolui8JzVnVlWh7ZmjCpnxw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.26.0", "", { "os": "none", "cpu": "x64" }, "sha512-0QfciUDFryD39QoSPUDshj4uNEjQhp73+3pbSAaxjV2qGOEDsM67P7KbJq7LzHoVl46oqhIhJ1S+skKGR7lMXA=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.26.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-vmAK+nHhIZWImwJ3RNw9hX3fU4UGN/OqbSE0imqljNbUQC3GvVJ1jpwYoTfD6mmXmQaxdJY6Hn4jQbLGJKg5Yw=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.26.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-GPXF7RMkJ7o9bTyUsnyNtrFMqgM3X+uM/LWw4CeHIjqc32fm0Ir6jKDnWHpj8xHFstgWDUYseSABK9KCkHGnpg=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.26.0", "", { "os": "none", "cpu": "arm64" }, "sha512-nUHZ5jEYqbBthbiBksbmHTlbb5eElyVfs/s1iHQ8rLBq1eWsd5maOnDpCocw1OM8kFK747d1Xms8dXJHtduxSw=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.26.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-TMg3KCTCYYaVO+R6P5mSORhcNDDlemUVnUbb8QkboUtOhb5JWKAzd5uMIMECJQOxHZ/R+N8HHtDF5ylzLfMiLw=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.26.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-apqYgoAUd6ZCb9Phcs8zN32q6l0ZQzQBdVXOofa6WvHDlSOhwCWgSfVQabGViThS40Y1NA4SCvQickgZMFZRlA=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.26.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-FGJAcImbJNZzLWu7U6WB0iKHl4RuY4TsXEwxJPl9UZLS47agIZuILZEX3Pagfw7I4J3ddflomt9f0apfaJSbaw=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.26.0", "", { "os": "win32", "cpu": "x64" }, "sha512-WAckBKaVnmFqbEhbymrPK7M086DQMpL1XoRbpmN0iW8k5JSXjDRQBhcZNa0VweItknLq9eAeCL34jK7/CDcw7A=="], + + "@inquirer/ansi": ["@inquirer/ansi@1.0.2", "", {}, "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ=="], + + "@inquirer/checkbox": ["@inquirer/checkbox@4.3.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/core": "^10.3.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA=="], + + "@inquirer/confirm": ["@inquirer/confirm@5.1.19", "", { "dependencies": { "@inquirer/core": "^10.3.0", "@inquirer/type": "^3.0.9" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-wQNz9cfcxrtEnUyG5PndC8g3gZ7lGDBzmWiXZkX8ot3vfZ+/BLjR8EvyGX4YzQLeVqtAlY/YScZpW7CW8qMoDQ=="], + + "@inquirer/core": ["@inquirer/core@10.3.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A=="], + + "@inquirer/editor": ["@inquirer/editor@4.2.23", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/external-editor": "^1.0.3", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ=="], + + "@inquirer/expand": ["@inquirer/expand@4.0.23", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew=="], + + "@inquirer/external-editor": ["@inquirer/external-editor@1.0.3", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA=="], + + "@inquirer/figures": ["@inquirer/figures@1.0.15", "", {}, "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g=="], + + "@inquirer/input": ["@inquirer/input@4.3.1", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g=="], + + "@inquirer/number": ["@inquirer/number@3.0.23", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg=="], + + "@inquirer/password": ["@inquirer/password@4.0.23", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA=="], + + "@inquirer/prompts": ["@inquirer/prompts@7.9.0", "", { "dependencies": { "@inquirer/checkbox": "^4.3.0", "@inquirer/confirm": "^5.1.19", "@inquirer/editor": "^4.2.21", "@inquirer/expand": "^4.0.21", "@inquirer/input": "^4.2.5", "@inquirer/number": "^3.0.21", "@inquirer/password": "^4.0.21", "@inquirer/rawlist": "^4.1.9", "@inquirer/search": "^3.2.0", "@inquirer/select": "^4.4.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-X7/+dG9SLpSzRkwgG5/xiIzW0oMrV3C0HOa7YHG1WnrLK+vCQHfte4k/T80059YBdei29RBC3s+pSMvPJDU9/A=="], + + "@inquirer/rawlist": ["@inquirer/rawlist@4.1.11", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw=="], + + "@inquirer/search": ["@inquirer/search@3.2.2", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA=="], + + "@inquirer/select": ["@inquirer/select@4.4.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/core": "^10.3.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w=="], + + "@inquirer/type": ["@inquirer/type@3.0.10", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA=="], + + "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="], + + "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="], + + "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + + "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], + + "@istanbuljs/schema": ["@istanbuljs/schema@0.1.3", "", {}, "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@listr2/prompt-adapter-inquirer": ["@listr2/prompt-adapter-inquirer@3.0.5", "", { "dependencies": { "@inquirer/type": "^3.0.8" }, "peerDependencies": { "@inquirer/prompts": ">= 3 < 8", "listr2": "9.0.5" } }, "sha512-WELs+hj6xcilkloBXYf9XXK8tYEnKsgLj01Xl5ONUJpKjmT5hGVUzNUS5tooUxs7pGMrw+jFD/41WpqW4V3LDA=="], + + "@lmdb/lmdb-darwin-arm64": ["@lmdb/lmdb-darwin-arm64@3.4.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zR6Y45VNtW5s+A+4AyhrJk0VJKhXdkLhrySCpCu7PSdnakebsOzNxf58p5Xoq66vOSuueGAxlqDAF49HwdrSTQ=="], + + "@lmdb/lmdb-darwin-x64": ["@lmdb/lmdb-darwin-x64@3.4.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-nfGm5pQksBGfaj9uMbjC0YyQreny/Pl7mIDtHtw6g7WQuCgeLullr9FNRsYyKplaEJBPrCVpEjpAznxTBIrXBw=="], + + "@lmdb/lmdb-linux-arm": ["@lmdb/lmdb-linux-arm@3.4.3", "", { "os": "linux", "cpu": "arm" }, "sha512-Kjqomp7i0rgSbYSUmv9JnXpS55zYT/YcW3Bdf9oqOTjcH0/8tFAP8MLhu/i9V2pMKIURDZk63Ww49DTK0T3c/Q=="], + + "@lmdb/lmdb-linux-arm64": ["@lmdb/lmdb-linux-arm64@3.4.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-uX9eaPqWb740wg5D3TCvU/js23lSRSKT7lJrrQ8IuEG/VLgpPlxO3lHDywU44yFYdGS7pElBn6ioKFKhvALZlw=="], + + "@lmdb/lmdb-linux-x64": ["@lmdb/lmdb-linux-x64@3.4.3", "", { "os": "linux", "cpu": "x64" }, "sha512-7/8l20D55CfwdMupkc3fNxNJdn4bHsti2X0cp6PwiXlLeSFvAfWs5kCCx+2Cyje4l4GtN//LtKWjTru/9hDJQg=="], + + "@lmdb/lmdb-win32-arm64": ["@lmdb/lmdb-win32-arm64@3.4.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-yWVR0e5Gl35EGJBsAuqPOdjtUYuN8CcTLKrqpQFoM+KsMadViVCulhKNhkcjSGJB88Am5bRPjMro4MBB9FS23Q=="], + + "@lmdb/lmdb-win32-x64": ["@lmdb/lmdb-win32-x64@3.4.3", "", { "os": "win32", "cpu": "x64" }, "sha512-1JdBkcO0Vrua4LUgr4jAe4FUyluwCeq/pDkBrlaVjX3/BBWP1TzVjCL+TibWNQtPAL1BITXPAhlK5Ru4FBd/hg=="], + + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.20.1", "", { "dependencies": { "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-j/P+yuxXfgxb+mW7OEoRCM3G47zCTDqUPivJo/VzpjbG8I9csTXtOprCf5FfOfHK4whOJny0aHuBEON+kS7CCA=="], + + "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw=="], + + "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw=="], + + "@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3", "", { "os": "linux", "cpu": "arm" }, "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw=="], + + "@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg=="], + + "@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg=="], + + "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ=="], + + "@napi-rs/nice": ["@napi-rs/nice@1.1.1", "", { "optionalDependencies": { "@napi-rs/nice-android-arm-eabi": "1.1.1", "@napi-rs/nice-android-arm64": "1.1.1", "@napi-rs/nice-darwin-arm64": "1.1.1", "@napi-rs/nice-darwin-x64": "1.1.1", "@napi-rs/nice-freebsd-x64": "1.1.1", "@napi-rs/nice-linux-arm-gnueabihf": "1.1.1", "@napi-rs/nice-linux-arm64-gnu": "1.1.1", "@napi-rs/nice-linux-arm64-musl": "1.1.1", "@napi-rs/nice-linux-ppc64-gnu": "1.1.1", "@napi-rs/nice-linux-riscv64-gnu": "1.1.1", "@napi-rs/nice-linux-s390x-gnu": "1.1.1", "@napi-rs/nice-linux-x64-gnu": "1.1.1", "@napi-rs/nice-linux-x64-musl": "1.1.1", "@napi-rs/nice-openharmony-arm64": "1.1.1", "@napi-rs/nice-win32-arm64-msvc": "1.1.1", "@napi-rs/nice-win32-ia32-msvc": "1.1.1", "@napi-rs/nice-win32-x64-msvc": "1.1.1" } }, "sha512-xJIPs+bYuc9ASBl+cvGsKbGrJmS6fAKaSZCnT0lhahT5rhA2VVy9/EcIgd2JhtEuFOJNx7UHNn/qiTPTY4nrQw=="], + + "@napi-rs/nice-android-arm-eabi": ["@napi-rs/nice-android-arm-eabi@1.1.1", "", { "os": "android", "cpu": "arm" }, "sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw=="], + + "@napi-rs/nice-android-arm64": ["@napi-rs/nice-android-arm64@1.1.1", "", { "os": "android", "cpu": "arm64" }, "sha512-blG0i7dXgbInN5urONoUCNf+DUEAavRffrO7fZSeoRMJc5qD+BJeNcpr54msPF6qfDD6kzs9AQJogZvT2KD5nw=="], + + "@napi-rs/nice-darwin-arm64": ["@napi-rs/nice-darwin-arm64@1.1.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-s/E7w45NaLqTGuOjC2p96pct4jRfo61xb9bU1unM/MJ/RFkKlJyJDx7OJI/O0ll/hrfpqKopuAFDV8yo0hfT7A=="], + + "@napi-rs/nice-darwin-x64": ["@napi-rs/nice-darwin-x64@1.1.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-dGoEBnVpsdcC+oHHmW1LRK5eiyzLwdgNQq3BmZIav+9/5WTZwBYX7r5ZkQC07Nxd3KHOCkgbHSh4wPkH1N1LiQ=="], + + "@napi-rs/nice-freebsd-x64": ["@napi-rs/nice-freebsd-x64@1.1.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kHv4kEHAylMYmlNwcQcDtXjklYp4FCf0b05E+0h6nDHsZ+F0bDe04U/tXNOqrx5CmIAth4vwfkjjUmp4c4JktQ=="], + + "@napi-rs/nice-linux-arm-gnueabihf": ["@napi-rs/nice-linux-arm-gnueabihf@1.1.1", "", { "os": "linux", "cpu": "arm" }, "sha512-E1t7K0efyKXZDoZg1LzCOLxgolxV58HCkaEkEvIYQx12ht2pa8hoBo+4OB3qh7e+QiBlp1SRf+voWUZFxyhyqg=="], + + "@napi-rs/nice-linux-arm64-gnu": ["@napi-rs/nice-linux-arm64-gnu@1.1.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-CIKLA12DTIZlmTaaKhQP88R3Xao+gyJxNWEn04wZwC2wmRapNnxCUZkVwggInMJvtVElA+D4ZzOU5sX4jV+SmQ=="], + + "@napi-rs/nice-linux-arm64-musl": ["@napi-rs/nice-linux-arm64-musl@1.1.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg=="], + + "@napi-rs/nice-linux-ppc64-gnu": ["@napi-rs/nice-linux-ppc64-gnu@1.1.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg=="], + + "@napi-rs/nice-linux-riscv64-gnu": ["@napi-rs/nice-linux-riscv64-gnu@1.1.1", "", { "os": "linux", "cpu": "none" }, "sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw=="], + + "@napi-rs/nice-linux-s390x-gnu": ["@napi-rs/nice-linux-s390x-gnu@1.1.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ=="], + + "@napi-rs/nice-linux-x64-gnu": ["@napi-rs/nice-linux-x64-gnu@1.1.1", "", { "os": "linux", "cpu": "x64" }, "sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg=="], + + "@napi-rs/nice-linux-x64-musl": ["@napi-rs/nice-linux-x64-musl@1.1.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw=="], + + "@napi-rs/nice-openharmony-arm64": ["@napi-rs/nice-openharmony-arm64@1.1.1", "", { "os": "none", "cpu": "arm64" }, "sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ=="], + + "@napi-rs/nice-win32-arm64-msvc": ["@napi-rs/nice-win32-arm64-msvc@1.1.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-uoTb4eAvM5B2aj/z8j+Nv8OttPf2m+HVx3UjA5jcFxASvNhQriyCQF1OB1lHL43ZhW+VwZlgvjmP5qF3+59atA=="], + + "@napi-rs/nice-win32-ia32-msvc": ["@napi-rs/nice-win32-ia32-msvc@1.1.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-CNQqlQT9MwuCsg1Vd/oKXiuH+TcsSPJmlAFc5frFyX/KkOh0UpBLEj7aoY656d5UKZQMQFP7vJNa1DNUNORvug=="], + + "@napi-rs/nice-win32-x64-msvc": ["@napi-rs/nice-win32-x64-msvc@1.1.1", "", { "os": "win32", "cpu": "x64" }, "sha512-vB+4G/jBQCAh0jelMTY3+kgFy00Hlx2f2/1zjMoH821IbplbWZOkLiTYXQkygNTzQJTq5cvwBDgn2ppHD+bglQ=="], + + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.7", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" } }, "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw=="], + + "@npmcli/agent": ["@npmcli/agent@4.0.0", "", { "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.1", "lru-cache": "^11.2.1", "socks-proxy-agent": "^8.0.3" } }, "sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA=="], + + "@npmcli/fs": ["@npmcli/fs@4.0.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q=="], + + "@npmcli/git": ["@npmcli/git@7.0.1", "", { "dependencies": { "@npmcli/promise-spawn": "^9.0.0", "ini": "^6.0.0", "lru-cache": "^11.2.1", "npm-pick-manifest": "^11.0.1", "proc-log": "^6.0.0", "promise-retry": "^2.0.1", "semver": "^7.3.5", "which": "^6.0.0" } }, "sha512-+XTFxK2jJF/EJJ5SoAzXk3qwIDfvFc5/g+bD274LZ7uY7LE8sTfG6Z8rOanPl2ZEvZWqNvmEdtXC25cE54VcoA=="], + + "@npmcli/installed-package-contents": ["@npmcli/installed-package-contents@3.0.0", "", { "dependencies": { "npm-bundled": "^4.0.0", "npm-normalize-package-bin": "^4.0.0" }, "bin": { "installed-package-contents": "bin/index.js" } }, "sha512-fkxoPuFGvxyrH+OQzyTkX2LUEamrF4jZSmxjAtPPHHGO0dqsQ8tTKjnIS8SAnPHdk2I03BDtSMR5K/4loKg79Q=="], + + "@npmcli/node-gyp": ["@npmcli/node-gyp@5.0.0", "", {}, "sha512-uuG5HZFXLfyFKqg8QypsmgLQW7smiRjVc45bqD/ofZZcR/uxEjgQU8qDPv0s9TEeMUiAAU/GC5bR6++UdTirIQ=="], + + "@npmcli/package-json": ["@npmcli/package-json@7.0.3", "", { "dependencies": { "@npmcli/git": "^7.0.0", "glob": "^12.0.0", "hosted-git-info": "^9.0.0", "json-parse-even-better-errors": "^5.0.0", "proc-log": "^6.0.0", "semver": "^7.5.3", "validate-npm-package-license": "^3.0.4" } }, "sha512-XT8016UrDfnR7yh2XvnIqaPnA5v2QomaWryDYYgKNT0LaX0vcKf4gu2f3CWD/ltV4tOto4MwZynWlynMJL8bBQ=="], + + "@npmcli/promise-spawn": ["@npmcli/promise-spawn@8.0.3", "", { "dependencies": { "which": "^5.0.0" } }, "sha512-Yb00SWaL4F8w+K8YGhQ55+xE4RUNdMHV43WZGsiTM92gS+lC0mGsn7I4hLug7pbao035S6bj3Y3w0cUNGLfmkg=="], + + "@npmcli/redact": ["@npmcli/redact@4.0.0", "", {}, "sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q=="], + + "@npmcli/run-script": ["@npmcli/run-script@10.0.3", "", { "dependencies": { "@npmcli/node-gyp": "^5.0.0", "@npmcli/package-json": "^7.0.0", "@npmcli/promise-spawn": "^9.0.0", "node-gyp": "^12.1.0", "proc-log": "^6.0.0", "which": "^6.0.0" } }, "sha512-ER2N6itRkzWbbtVmZ9WKaWxVlKlOeBFF1/7xx+KA5J1xKa4JjUwBdb6tDpk0v1qA+d+VDwHI9qmLcXSWcmi+Rw=="], + + "@oxc-project/types": ["@oxc-project/types@0.96.0", "", {}, "sha512-r/xkmoXA0xEpU6UGtn18CNVjXH6erU3KCpCDbpLmbVxBFor1U9MqN5Z2uMmCHJuXjJzlnDR+hWY+yPoLo8oHDw=="], + + "@parcel/watcher": ["@parcel/watcher@2.5.1", "", { "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", "micromatch": "^4.0.5", "node-addon-api": "^7.0.0" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-freebsd-x64": "2.5.1", "@parcel/watcher-linux-arm-glibc": "2.5.1", "@parcel/watcher-linux-arm-musl": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", "@parcel/watcher-linux-arm64-musl": "2.5.1", "@parcel/watcher-linux-x64-glibc": "2.5.1", "@parcel/watcher-linux-x64-musl": "2.5.1", "@parcel/watcher-win32-arm64": "2.5.1", "@parcel/watcher-win32-ia32": "2.5.1", "@parcel/watcher-win32-x64": "2.5.1" } }, "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg=="], + + "@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.1", "", { "os": "android", "cpu": "arm64" }, "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA=="], + + "@parcel/watcher-darwin-arm64": ["@parcel/watcher-darwin-arm64@2.5.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw=="], + + "@parcel/watcher-darwin-x64": ["@parcel/watcher-darwin-x64@2.5.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg=="], + + "@parcel/watcher-freebsd-x64": ["@parcel/watcher-freebsd-x64@2.5.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ=="], + + "@parcel/watcher-linux-arm-glibc": ["@parcel/watcher-linux-arm-glibc@2.5.1", "", { "os": "linux", "cpu": "arm" }, "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA=="], + + "@parcel/watcher-linux-arm-musl": ["@parcel/watcher-linux-arm-musl@2.5.1", "", { "os": "linux", "cpu": "arm" }, "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q=="], + + "@parcel/watcher-linux-arm64-glibc": ["@parcel/watcher-linux-arm64-glibc@2.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w=="], + + "@parcel/watcher-linux-arm64-musl": ["@parcel/watcher-linux-arm64-musl@2.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg=="], + + "@parcel/watcher-linux-x64-glibc": ["@parcel/watcher-linux-x64-glibc@2.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A=="], + + "@parcel/watcher-linux-x64-musl": ["@parcel/watcher-linux-x64-musl@2.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg=="], + + "@parcel/watcher-win32-arm64": ["@parcel/watcher-win32-arm64@2.5.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw=="], + + "@parcel/watcher-win32-ia32": ["@parcel/watcher-win32-ia32@2.5.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ=="], + + "@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="], + + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-beta.47", "", { "os": "android", "cpu": "arm64" }, "sha512-vPP9/MZzESh9QtmvQYojXP/midjgkkc1E4AdnPPAzQXo668ncHJcVLKjJKzoBdsQmaIvNjrMdsCwES8vTQHRQw=="], + + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-beta.47", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Lc3nrkxeaDVCVl8qR3qoxh6ltDZfkQ98j5vwIr5ALPkgjZtDK4BGCrrBoLpGVMg+csWcaqUbwbKwH5yvVa0oOw=="], + + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-beta.47", "", { "os": "darwin", "cpu": "x64" }, "sha512-eBYxQDwP0O33plqNVqOtUHqRiSYVneAknviM5XMawke3mwMuVlAsohtOqEjbCEl/Loi/FWdVeks5WkqAkzkYWQ=="], + + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-beta.47", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Ns+kgp2+1Iq/44bY/Z30DETUSiHY7ZuqaOgD5bHVW++8vme9rdiWsN4yG4rRPXkdgzjvQ9TDHmZZKfY4/G11AA=="], + + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.47", "", { "os": "linux", "cpu": "arm" }, "sha512-4PecgWCJhTA2EFOlptYJiNyVP2MrVP4cWdndpOu3WmXqWqZUmSubhb4YUAIxAxnXATlGjC1WjxNPhV7ZllNgdA=="], + + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-beta.47", "", { "os": "linux", "cpu": "arm64" }, "sha512-CyIunZ6D9U9Xg94roQI1INt/bLkOpPsZjZZkiaAZ0r6uccQdICmC99M9RUPlMLw/qg4yEWLlQhG73W/mG437NA=="], + + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-beta.47", "", { "os": "linux", "cpu": "arm64" }, "sha512-doozc/Goe7qRCSnzfJbFINTHsMktqmZQmweull6hsZZ9sjNWQ6BWQnbvOlfZJe4xE5NxM1NhPnY5Giqnl3ZrYQ=="], + + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-beta.47", "", { "os": "linux", "cpu": "x64" }, "sha512-fodvSMf6Aqwa0wEUSTPewmmZOD44rc5Tpr5p9NkwQ6W1SSpUKzD3SwpJIgANDOhwiYhDuiIaYPGB7Ujkx1q0UQ=="], + + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-beta.47", "", { "os": "linux", "cpu": "x64" }, "sha512-Rxm5hYc0mGjwLh5sjlGmMygxAaV2gnsx7CNm2lsb47oyt5UQyPDZf3GP/ct8BEcwuikdqzsrrlIp8+kCSvMFNQ=="], + + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-beta.47", "", { "os": "none", "cpu": "arm64" }, "sha512-YakuVe+Gc87jjxazBL34hbr8RJpRuFBhun7NEqoChVDlH5FLhLXjAPHqZd990TVGVNkemourf817Z8u2fONS8w=="], + + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-beta.47", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.0.7" }, "cpu": "none" }, "sha512-ak2GvTFQz3UAOw8cuQq8pWE+TNygQB6O47rMhvevvTzETh7VkHRFtRUwJynX5hwzFvQMP6G0az5JrBGuwaMwYQ=="], + + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-beta.47", "", { "os": "win32", "cpu": "arm64" }, "sha512-o5BpmBnXU+Cj+9+ndMcdKjhZlPb79dVPBZnWwMnI4RlNSSq5yOvFZqvfPYbyacvnW03Na4n5XXQAPhu3RydZ0w=="], + + "@rolldown/binding-win32-ia32-msvc": ["@rolldown/binding-win32-ia32-msvc@1.0.0-beta.47", "", { "os": "win32", "cpu": "ia32" }, "sha512-FVOmfyYehNE92IfC9Kgs913UerDog2M1m+FADJypKz0gmRg3UyTt4o1cZMCAl7MiR89JpM9jegNO1nXuP1w1vw=="], + + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-beta.47", "", { "os": "win32", "cpu": "x64" }, "sha512-by/70F13IUE101Bat0oeH8miwWX5mhMFPk1yjCdxoTNHTyTdLgb0THNaebRM6AP7Kz+O3O2qx87sruYuF5UxHg=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.47", "", {}, "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.53.3", "", { "os": "android", "cpu": "arm" }, "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.53.3", "", { "os": "android", "cpu": "arm64" }, "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.53.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.53.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.53.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.53.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.53.3", "", { "os": "linux", "cpu": "arm" }, "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.53.3", "", { "os": "linux", "cpu": "arm" }, "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.53.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.53.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.53.3", "", { "os": "linux", "cpu": "none" }, "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.53.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.53.3", "", { "os": "linux", "cpu": "none" }, "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.53.3", "", { "os": "linux", "cpu": "none" }, "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.53.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.53.3", "", { "os": "linux", "cpu": "x64" }, "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.53.3", "", { "os": "linux", "cpu": "x64" }, "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.53.3", "", { "os": "none", "cpu": "arm64" }, "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.53.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.53.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.53.3", "", { "os": "win32", "cpu": "x64" }, "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.53.3", "", { "os": "win32", "cpu": "x64" }, "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ=="], + + "@schematics/angular": ["@schematics/angular@21.0.0", "", { "dependencies": { "@angular-devkit/core": "21.0.0", "@angular-devkit/schematics": "21.0.0", "jsonc-parser": "3.3.1" } }, "sha512-50eEsBaT++Gwr+5FAhaKIzTUjpE1DJAwmE5QwtogbTnr2viZc8CsbFOfuMrokQbgdcXRvbkBDPXgO15STMcDRQ=="], + + "@sigstore/bundle": ["@sigstore/bundle@4.0.0", "", { "dependencies": { "@sigstore/protobuf-specs": "^0.5.0" } }, "sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A=="], + + "@sigstore/core": ["@sigstore/core@3.0.0", "", {}, "sha512-NgbJ+aW9gQl/25+GIEGYcCyi8M+ng2/5X04BMuIgoDfgvp18vDcoNHOQjQsG9418HGNYRxG3vfEXaR1ayD37gg=="], + + "@sigstore/protobuf-specs": ["@sigstore/protobuf-specs@0.5.0", "", {}, "sha512-MM8XIwUjN2bwvCg1QvrMtbBmpcSHrkhFSCu1D11NyPvDQ25HEc4oG5/OcQfd/Tlf/OxmKWERDj0zGE23jQaMwA=="], + + "@sigstore/sign": ["@sigstore/sign@4.0.1", "", { "dependencies": { "@sigstore/bundle": "^4.0.0", "@sigstore/core": "^3.0.0", "@sigstore/protobuf-specs": "^0.5.0", "make-fetch-happen": "^15.0.2", "proc-log": "^5.0.0", "promise-retry": "^2.0.1" } }, "sha512-KFNGy01gx9Y3IBPG/CergxR9RZpN43N+lt3EozEfeoyqm8vEiLxwRl3ZO5sPx3Obv1ix/p7FWOlPc2Jgwfp9PA=="], + + "@sigstore/tuf": ["@sigstore/tuf@4.0.0", "", { "dependencies": { "@sigstore/protobuf-specs": "^0.5.0", "tuf-js": "^4.0.0" } }, "sha512-0QFuWDHOQmz7t66gfpfNO6aEjoFrdhkJaej/AOqb4kqWZVbPWFZifXZzkxyQBB1OwTbkhdT3LNpMFxwkTvf+2w=="], + + "@sigstore/verify": ["@sigstore/verify@3.0.0", "", { "dependencies": { "@sigstore/bundle": "^4.0.0", "@sigstore/core": "^3.0.0", "@sigstore/protobuf-specs": "^0.5.0" } }, "sha512-moXtHH33AobOhTZF8xcX1MpOFqdvfCk7v6+teJL8zymBiDXwEsQH6XG9HGx2VIxnJZNm4cNSzflTLDnQLmIdmw=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], + + "@tailwindcss/node": ["@tailwindcss/node@4.1.17", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.17" } }, "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg=="], + + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.17", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.17", "@tailwindcss/oxide-darwin-arm64": "4.1.17", "@tailwindcss/oxide-darwin-x64": "4.1.17", "@tailwindcss/oxide-freebsd-x64": "4.1.17", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.17", "@tailwindcss/oxide-linux-arm64-musl": "4.1.17", "@tailwindcss/oxide-linux-x64-gnu": "4.1.17", "@tailwindcss/oxide-linux-x64-musl": "4.1.17", "@tailwindcss/oxide-wasm32-wasi": "4.1.17", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.17", "@tailwindcss/oxide-win32-x64-msvc": "4.1.17" } }, "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA=="], + + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.17", "", { "os": "android", "cpu": "arm64" }, "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ=="], + + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.17", "", { "os": "darwin", "cpu": "arm64" }, "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg=="], + + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.17", "", { "os": "darwin", "cpu": "x64" }, "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog=="], + + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.17", "", { "os": "freebsd", "cpu": "x64" }, "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g=="], + + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17", "", { "os": "linux", "cpu": "arm" }, "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ=="], + + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ=="], + + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg=="], + + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.17", "", { "os": "linux", "cpu": "x64" }, "sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ=="], + + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.17", "", { "os": "linux", "cpu": "x64" }, "sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ=="], + + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.17", "", { "cpu": "none" }, "sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg=="], + + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.17", "", { "os": "win32", "cpu": "arm64" }, "sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A=="], + + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.17", "", { "os": "win32", "cpu": "x64" }, "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw=="], + + "@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.17", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.17", "@tailwindcss/oxide": "4.1.17", "postcss": "^8.4.41", "tailwindcss": "4.1.17" } }, "sha512-+nKl9N9mN5uJ+M7dBOOCzINw94MPstNR/GtIhz1fpZysxL/4a+No64jCBD6CPN+bIHWFx3KWuu8XJRrj/572Dw=="], + + "@tauri-apps/api": ["@tauri-apps/api@2.10.1", "", {}, "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw=="], + + "@tauri-apps/cli": ["@tauri-apps/cli@2.10.1", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.10.1", "@tauri-apps/cli-darwin-x64": "2.10.1", "@tauri-apps/cli-linux-arm-gnueabihf": "2.10.1", "@tauri-apps/cli-linux-arm64-gnu": "2.10.1", "@tauri-apps/cli-linux-arm64-musl": "2.10.1", "@tauri-apps/cli-linux-riscv64-gnu": "2.10.1", "@tauri-apps/cli-linux-x64-gnu": "2.10.1", "@tauri-apps/cli-linux-x64-musl": "2.10.1", "@tauri-apps/cli-win32-arm64-msvc": "2.10.1", "@tauri-apps/cli-win32-ia32-msvc": "2.10.1", "@tauri-apps/cli-win32-x64-msvc": "2.10.1" }, "bin": { "tauri": "tauri.js" } }, "sha512-jQNGF/5quwORdZSSLtTluyKQ+o6SMa/AUICfhf4egCGFdMHqWssApVgYSbg+jmrZoc8e1DscNvjTnXtlHLS11g=="], + + "@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.10.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Z2OjCXiZ+fbYZy7PmP3WRnOpM9+Fy+oonKDEmUE6MwN4IGaYqgceTjwHucc/kEEYZos5GICve35f7ZiizgqEnQ=="], + + "@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.10.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-V/irQVvjPMGOTQqNj55PnQPVuH4VJP8vZCN7ajnj+ZS8Kom1tEM2hR3qbbIRoS3dBKs5mbG8yg1WC+97dq17Pw=="], + + "@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.10.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Hyzwsb4VnCWKGfTw+wSt15Z2pLw2f0JdFBfq2vHBOBhvg7oi6uhKiF87hmbXOBXUZaGkyRDkCHsdzJcIfoJC2w=="], + + "@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.10.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-OyOYs2t5GkBIvyWjA1+h4CZxTcdz1OZPCWAPz5DYEfB0cnWHERTnQ/SLayQzncrT0kwRoSfSz9KxenkyJoTelA=="], + + "@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.10.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-MIj78PDDGjkg3NqGptDOGgfXks7SYJwhiMh8SBoZS+vfdz7yP5jN18bNaLnDhsVIPARcAhE1TlsZe/8Yxo2zqg=="], + + "@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.10.1", "", { "os": "linux", "cpu": "none" }, "sha512-X0lvOVUg8PCVaoEtEAnpxmnkwlE1gcMDTqfhbefICKDnOTJ5Est3qL0SrWxizDackIOKBcvtpejrSiVpuJI1kw=="], + + "@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.10.1", "", { "os": "linux", "cpu": "x64" }, "sha512-2/12bEzsJS9fAKybxgicCDFxYD1WEI9kO+tlDwX5znWG2GwMBaiWcmhGlZ8fi+DMe9CXlcVarMTYc0L3REIRxw=="], + + "@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.10.1", "", { "os": "linux", "cpu": "x64" }, "sha512-Y8J0ZzswPz50UcGOFuXGEMrxbjwKSPgXftx5qnkuMs2rmwQB5ssvLb6tn54wDSYxe7S6vlLob9vt0VKuNOaCIQ=="], + + "@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.10.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-iSt5B86jHYAPJa/IlYw++SXtFPGnWtFJriHn7X0NFBVunF6zu9+/zOn8OgqIWSl8RgzhLGXQEEtGBdR4wzpVgg=="], + + "@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.10.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-gXyxgEzsFegmnWywYU5pEBURkcFN/Oo45EAwvZrHMh+zUSEAvO5E8TXsgPADYm31d1u7OQU3O3HsYfVBf2moHw=="], + + "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.10.1", "", { "os": "win32", "cpu": "x64" }, "sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg=="], + + "@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.7.0", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-4nS/hfGMGCXiAS3LtVjH9AgsSAPJeG/7R+q8agTFqytjnMa4Zq95Bq8WzVDkckpanX+yyRHXnRtrKXkANKDHvw=="], + + "@tufjs/canonical-json": ["@tufjs/canonical-json@2.0.0", "", {}, "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA=="], + + "@tufjs/models": ["@tufjs/models@4.0.0", "", { "dependencies": { "@tufjs/canonical-json": "2.0.0", "minimatch": "^9.0.5" } }, "sha512-h5x5ga/hh82COe+GoD4+gKUeV4T3iaYOxqLt41GRKApinPI7DMidhCmNVTjKfhCWFJIGXaFJee07XczdT4jdZQ=="], + + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@vitejs/plugin-basic-ssl": ["@vitejs/plugin-basic-ssl@2.1.0", "", { "peerDependencies": { "vite": "^6.0.0 || ^7.0.0" } }, "sha512-dOxxrhgyDIEUADhb/8OlV9JIqYLgos03YorAueTIeOUskLJSEsfwCByjbu98ctXitUN3znXKp0bYD/WHSudCeA=="], + + "@vitest/expect": ["@vitest/expect@4.0.12", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.12", "@vitest/utils": "4.0.12", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-is+g0w8V3/ZhRNrRizrJNr8PFQKwYmctWlU4qg8zy5r9aIV5w8IxXLlfbbxJCwSpsVl2PXPTm2/zruqTqz3QSg=="], + + "@vitest/mocker": ["@vitest/mocker@4.0.12", "", { "dependencies": { "@vitest/spy": "4.0.12", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw"] }, "sha512-GsmA/tD5Ht3RUFoz41mZsMU1AXch3lhmgbTnoSPTdH231g7S3ytNN1aU0bZDSyxWs8WA7KDyMPD5L4q6V6vj9w=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@4.0.12", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-R7nMAcnienG17MvRN8TPMJiCG8rrZJblV9mhT7oMFdBXvS0x+QD6S1G4DxFusR2E0QIS73f7DqSR1n87rrmE+g=="], + + "@vitest/runner": ["@vitest/runner@4.0.12", "", { "dependencies": { "@vitest/utils": "4.0.12", "pathe": "^2.0.3" } }, "sha512-hDlCIJWuwlcLumfukPsNfPDOJokTv79hnOlf11V+n7E14rHNPz0Sp/BO6h8sh9qw4/UjZiKyYpVxK2ZNi+3ceQ=="], + + "@vitest/snapshot": ["@vitest/snapshot@4.0.12", "", { "dependencies": { "@vitest/pretty-format": "4.0.12", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-2jz9zAuBDUSbnfyixnyOd1S2YDBrZO23rt1bicAb6MA/ya5rHdKFRikPIDpBj/Dwvh6cbImDmudegnDAkHvmRQ=="], + + "@vitest/spy": ["@vitest/spy@4.0.12", "", {}, "sha512-GZjI9PPhiOYNX8Nsyqdw7JQB+u0BptL5fSnXiottAUBHlcMzgADV58A7SLTXXQwcN1yZ6gfd1DH+2bqjuUlCzw=="], + + "@vitest/utils": ["@vitest/utils@4.0.12", "", { "dependencies": { "@vitest/pretty-format": "4.0.12", "tinyrainbow": "^3.0.3" } }, "sha512-DVS/TLkLdvGvj1avRy0LSmKfrcI9MNFvNGN6ECjTUHWJdlcgPDOXhjMis5Dh7rBH62nAmSXnkPbE+DZ5YD75Rw=="], + + "@yarnpkg/lockfile": ["@yarnpkg/lockfile@1.1.0", "", {}, "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ=="], + + "abbrev": ["abbrev@4.0.0", "", {}, "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA=="], + + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + + "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" }, "peerDependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + + "algoliasearch": ["algoliasearch@5.40.1", "", { "dependencies": { "@algolia/abtesting": "1.6.1", "@algolia/client-abtesting": "5.40.1", "@algolia/client-analytics": "5.40.1", "@algolia/client-common": "5.40.1", "@algolia/client-insights": "5.40.1", "@algolia/client-personalization": "5.40.1", "@algolia/client-query-suggestions": "5.40.1", "@algolia/client-search": "5.40.1", "@algolia/ingestion": "1.40.1", "@algolia/monitoring": "1.40.1", "@algolia/recommend": "5.40.1", "@algolia/requester-browser-xhr": "5.40.1", "@algolia/requester-fetch": "5.40.1", "@algolia/requester-node-http": "5.40.1" } }, "sha512-iUNxcXUNg9085TJx0HJLjqtDE0r1RZ0GOGrt8KNQqQT5ugu8lZsHuMUYW/e0lHhq6xBvmktU9Bw4CXP9VQeKrg=="], + + "ansi-escapes": ["ansi-escapes@7.2.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw=="], + + "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.8.30", "", { "bin": "dist/cli.js" }, "sha512-aTUKW4ptQhS64+v2d6IkPzymEzzhw+G0bA1g3uBRV3+ntkH+svttKseW5IOR4Ed6NUVKqnY7qT3dKvzQ7io4AA=="], + + "beasties": ["beasties@0.3.5", "", { "dependencies": { "css-select": "^6.0.0", "css-what": "^7.0.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "htmlparser2": "^10.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.49", "postcss-media-query-parser": "^0.2.3" } }, "sha512-NaWu+f4YrJxEttJSm16AzMIFtVldCvaJ68b1L098KpqXmxt9xOLtKoLkKxb8ekhOrLqEJAbvT6n6SEvB/sac7A=="], + + "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], + + "body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="], + + "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], + + "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": "cli.js" }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], + + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "cacache": ["cacache@20.0.2", "", { "dependencies": { "@npmcli/fs": "^4.0.0", "fs-minipass": "^3.0.0", "glob": "^11.0.3", "lru-cache": "^11.1.0", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^7.0.2", "ssri": "^13.0.0", "unique-filename": "^4.0.0" } }, "sha512-rVWvqtWcgSzB22wImrVto+7PmE+lUqv5dYzRHD0QJsfpSwTkW+GIqA4ykSt/CCjQlQle8USn8CO8vcWNrIqktg=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001756", "", {}, "sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A=="], + + "chai": ["chai@6.2.1", "", {}, "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg=="], + + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="], + + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + + "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], + + "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], + + "cli-spinners": ["cli-spinners@3.3.0", "", {}, "sha512-/+40ljC3ONVnYIttjMWrlL51nItDAbBrq2upN8BPyvGU/2n5Oxw3tbNwORCaNuNqLJnxGqOfjUuhsv7l5Q4IsQ=="], + + "cli-truncate": ["cli-truncate@5.1.1", "", { "dependencies": { "slice-ansi": "^7.1.0", "string-width": "^8.0.0" } }, "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A=="], + + "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], + + "cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], + + "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="], + + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + + "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "css-select": ["css-select@6.0.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^7.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "nth-check": "^2.1.1" } }, "sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw=="], + + "css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="], + + "css-what": ["css-what@7.0.0", "", {}, "sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ=="], + + "cssstyle": ["cssstyle@5.3.3", "", { "dependencies": { "@asamuzakjp/css-color": "^4.0.3", "@csstools/css-syntax-patches-for-csstree": "^1.0.14", "css-tree": "^3.1.0" } }, "sha512-OytmFH+13/QXONJcC75QNdMtKpceNk3u8ThBjyyYjkEcy/ekBwR1mMAuNvi3gdBPW3N5TlCzQ0WZw8H0lN/bDw=="], + + "data-urls": ["data-urls@6.0.0", "", { "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^15.0.0" } }, "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], + + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], + + "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], + + "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], + + "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.258", "", {}, "sha512-rHUggNV5jKQ0sSdWwlaRDkFc3/rRJIVnOSe9yR4zrR07m3ZxhP4N27Hlg8VeJGGYgFTxK5NqDmWI4DSH72vIJg=="], + + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "encoding": ["encoding@0.1.13", "", { "dependencies": { "iconv-lite": "^0.6.2" } }, "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A=="], + + "enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="], + + "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + + "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], + + "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], + + "err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "esbuild": ["esbuild@0.26.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.26.0", "@esbuild/android-arm": "0.26.0", "@esbuild/android-arm64": "0.26.0", "@esbuild/android-x64": "0.26.0", "@esbuild/darwin-arm64": "0.26.0", "@esbuild/darwin-x64": "0.26.0", "@esbuild/freebsd-arm64": "0.26.0", "@esbuild/freebsd-x64": "0.26.0", "@esbuild/linux-arm": "0.26.0", "@esbuild/linux-arm64": "0.26.0", "@esbuild/linux-ia32": "0.26.0", "@esbuild/linux-loong64": "0.26.0", "@esbuild/linux-mips64el": "0.26.0", "@esbuild/linux-ppc64": "0.26.0", "@esbuild/linux-riscv64": "0.26.0", "@esbuild/linux-s390x": "0.26.0", "@esbuild/linux-x64": "0.26.0", "@esbuild/netbsd-arm64": "0.26.0", "@esbuild/netbsd-x64": "0.26.0", "@esbuild/openbsd-arm64": "0.26.0", "@esbuild/openbsd-x64": "0.26.0", "@esbuild/openharmony-arm64": "0.26.0", "@esbuild/sunos-x64": "0.26.0", "@esbuild/win32-arm64": "0.26.0", "@esbuild/win32-ia32": "0.26.0", "@esbuild/win32-x64": "0.26.0" }, "bin": "bin/esbuild" }, "sha512-3Hq7jri+tRrVWha+ZeIVhl4qJRha/XjRNSopvTsOaCvfPHrflTYTcUFcEjMKdxofsXXsdc4zjg5NOTnL4Gl57Q=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], + + "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], + + "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], + + "expect-type": ["expect-type@1.2.2", "", {}, "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA=="], + + "exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="], + + "express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="], + + "express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" } }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="], + + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + + "fs-minipass": ["fs-minipass@3.0.3", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "glob": ["glob@12.0.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": "dist/esm/bin.mjs" }, "sha512-5Qcll1z7IKgHr5g485ePDdHcNQY0k2dtv/bjYy0iuyGxQw2qSOiiXUXJ+AYQpg3HNoUMHqAruX478Jeev7UULw=="], + + "glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "hosted-git-info": ["hosted-git-info@9.0.2", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg=="], + + "html-encoding-sniffer": ["html-encoding-sniffer@4.0.0", "", { "dependencies": { "whatwg-encoding": "^3.1.1" } }, "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ=="], + + "htmlparser2": ["htmlparser2@10.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.1", "entities": "^6.0.0" } }, "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g=="], + + "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="], + + "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], + + "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], + + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + + "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + + "ignore-walk": ["ignore-walk@8.0.0", "", { "dependencies": { "minimatch": "^10.0.3" } }, "sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A=="], + + "immutable": ["immutable@5.1.4", "", {}, "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ini": ["ini@5.0.0", "", {}, "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw=="], + + "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], + + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + + "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="], + + "istanbul-lib-instrument": ["istanbul-lib-instrument@6.0.3", "", { "dependencies": { "@babel/core": "^7.23.9", "@babel/parser": "^7.23.9", "@istanbuljs/schema": "^0.1.3", "istanbul-lib-coverage": "^3.2.0", "semver": "^7.5.4" } }, "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q=="], + + "jackspeak": ["jackspeak@4.1.1", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" } }, "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ=="], + + "jiti": ["jiti@2.6.1", "", { "bin": "lib/jiti-cli.mjs" }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "jsdom": ["jsdom@27.2.0", "", { "dependencies": { "@acemir/cssom": "^0.9.23", "@asamuzakjp/dom-selector": "^6.7.4", "cssstyle": "^5.3.3", "data-urls": "^6.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "parse5": "^8.0.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^15.1.0", "ws": "^8.18.3", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-454TI39PeRDW1LgpyLPyURtB4Zx1tklSr6+OFOipsxGUH1WMTvk6C65JQdrj455+DP2uJ1+veBEHTGFKWVLFoA=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": "bin/jsesc" }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json-parse-even-better-errors": ["json-parse-even-better-errors@5.0.0", "", {}, "sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ=="], + + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "json5": ["json5@2.2.3", "", { "bin": "lib/cli.js" }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], + + "jsonparse": ["jsonparse@1.3.1", "", {}, "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg=="], + + "lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], + + "listr2": ["listr2@9.0.5", "", { "dependencies": { "cli-truncate": "^5.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g=="], + + "lmdb": ["lmdb@3.4.3", "", { "dependencies": { "msgpackr": "^1.11.2", "node-addon-api": "^6.1.0", "node-gyp-build-optional-packages": "5.2.2", "ordered-binary": "^1.5.3", "weak-lru-cache": "^1.2.2" }, "optionalDependencies": { "@lmdb/lmdb-darwin-arm64": "3.4.3", "@lmdb/lmdb-darwin-x64": "3.4.3", "@lmdb/lmdb-linux-arm": "3.4.3", "@lmdb/lmdb-linux-arm64": "3.4.3", "@lmdb/lmdb-linux-x64": "3.4.3", "@lmdb/lmdb-win32-arm64": "3.4.3", "@lmdb/lmdb-win32-x64": "3.4.3" }, "bin": { "download-lmdb-prebuilds": "bin/download-prebuilds.js" } }, "sha512-GWV1kVi6uhrXWqe+3NXWO73OYe8fto6q8JMo0HOpk1vf8nEyFWgo4CSNJpIFzsOxOrysVUlcO48qRbQfmKd1gA=="], + + "log-symbols": ["log-symbols@7.0.1", "", { "dependencies": { "is-unicode-supported": "^2.0.0", "yoctocolors": "^2.1.1" } }, "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg=="], + + "log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="], + + "lru-cache": ["lru-cache@11.2.2", "", {}, "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg=="], + + "magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="], + + "make-fetch-happen": ["make-fetch-happen@15.0.3", "", { "dependencies": { "@npmcli/agent": "^4.0.0", "cacache": "^20.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", "minipass-fetch": "^5.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^6.0.0", "promise-retry": "^2.0.1", "ssri": "^13.0.0" } }, "sha512-iyyEpDty1mwW3dGlYXAJqC/azFn5PPvgKVwXayOGBSmKLxhKZ9fg4qIan2ePpp1vJIwfFiO34LAPZgq9SZW9Aw=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="], + + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + + "minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], + + "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + + "minipass-collect": ["minipass-collect@2.0.1", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw=="], + + "minipass-fetch": ["minipass-fetch@5.0.0", "", { "dependencies": { "minipass": "^7.0.3", "minipass-sized": "^1.0.3", "minizlib": "^3.0.1" }, "optionalDependencies": { "encoding": "^0.1.13" } }, "sha512-fiCdUALipqgPWrOVTz9fw0XhcazULXOSU6ie40DDbX1F49p1dBrSRBuswndTx1x3vEb/g0FT7vC4c4C2u/mh3A=="], + + "minipass-flush": ["minipass-flush@1.0.5", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw=="], + + "minipass-pipeline": ["minipass-pipeline@1.2.4", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A=="], + + "minipass-sized": ["minipass-sized@1.0.3", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g=="], + + "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], + + "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "msgpackr": ["msgpackr@1.11.5", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA=="], + + "msgpackr-extract": ["msgpackr-extract@3.0.3", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA=="], + + "mute-stream": ["mute-stream@2.0.0", "", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": "bin/nanoid.cjs" }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + + "node-addon-api": ["node-addon-api@6.1.0", "", {}, "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA=="], + + "node-gyp": ["node-gyp@12.1.0", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", "make-fetch-happen": "^15.0.0", "nopt": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", "tar": "^7.5.2", "tinyglobby": "^0.2.12", "which": "^6.0.0" }, "bin": "bin/node-gyp.js" }, "sha512-W+RYA8jBnhSr2vrTtlPYPc1K+CSjGpVDRZxcqJcERZ8ND3A1ThWPHRwctTx3qC3oW99jt726jhdz3Y6ky87J4g=="], + + "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], + + "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], + + "nopt": ["nopt@9.0.0", "", { "dependencies": { "abbrev": "^4.0.0" }, "bin": "bin/nopt.js" }, "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw=="], + + "npm-bundled": ["npm-bundled@4.0.0", "", { "dependencies": { "npm-normalize-package-bin": "^4.0.0" } }, "sha512-IxaQZDMsqfQ2Lz37VvyyEtKLe8FsRZuysmedy/N06TU1RyVppYKXrO4xIhR0F+7ubIBox6Q7nir6fQI3ej39iA=="], + + "npm-install-checks": ["npm-install-checks@8.0.0", "", { "dependencies": { "semver": "^7.1.1" } }, "sha512-ScAUdMpyzkbpxoNekQ3tNRdFI8SJ86wgKZSQZdUxT+bj0wVFpsEMWnkXP0twVe1gJyNF5apBWDJhhIbgrIViRA=="], + + "npm-normalize-package-bin": ["npm-normalize-package-bin@4.0.0", "", {}, "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w=="], + + "npm-package-arg": ["npm-package-arg@13.0.1", "", { "dependencies": { "hosted-git-info": "^9.0.0", "proc-log": "^5.0.0", "semver": "^7.3.5", "validate-npm-package-name": "^6.0.0" } }, "sha512-6zqls5xFvJbgFjB1B2U6yITtyGBjDBORB7suI4zA4T/sZ1OmkMFlaQSNB/4K0LtXNA1t4OprAFxPisadK5O2ag=="], + + "npm-packlist": ["npm-packlist@10.0.3", "", { "dependencies": { "ignore-walk": "^8.0.0", "proc-log": "^6.0.0" } }, "sha512-zPukTwJMOu5X5uvm0fztwS5Zxyvmk38H/LfidkOMt3gbZVCyro2cD/ETzwzVPcWZA3JOyPznfUN/nkyFiyUbxg=="], + + "npm-pick-manifest": ["npm-pick-manifest@11.0.3", "", { "dependencies": { "npm-install-checks": "^8.0.0", "npm-normalize-package-bin": "^5.0.0", "npm-package-arg": "^13.0.0", "semver": "^7.3.5" } }, "sha512-buzyCfeoGY/PxKqmBqn1IUJrZnUi1VVJTdSSRPGI60tJdUhUoSQFhs0zycJokDdOznQentgrpf8LayEHyyYlqQ=="], + + "npm-registry-fetch": ["npm-registry-fetch@19.1.1", "", { "dependencies": { "@npmcli/redact": "^4.0.0", "jsonparse": "^1.3.1", "make-fetch-happen": "^15.0.0", "minipass": "^7.0.2", "minipass-fetch": "^5.0.0", "minizlib": "^3.0.1", "npm-package-arg": "^13.0.0", "proc-log": "^6.0.0" } }, "sha512-TakBap6OM1w0H73VZVDf44iFXsOS3h+L4wVMXmbWOQroZgFhMch0juN6XSzBNlD965yIKvWg2dfu7NSiaYLxtw=="], + + "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + + "ora": ["ora@9.0.0", "", { "dependencies": { "chalk": "^5.6.2", "cli-cursor": "^5.0.0", "cli-spinners": "^3.2.0", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", "stdin-discarder": "^0.2.2", "string-width": "^8.1.0", "strip-ansi": "^7.1.2" } }, "sha512-m0pg2zscbYgWbqRR6ABga5c3sZdEon7bSgjnlXC64kxtxLOyjRcbbUkLj7HFyy/FTD+P2xdBWu8snGhYI0jc4A=="], + + "ordered-binary": ["ordered-binary@1.6.0", "", {}, "sha512-IQh2aMfMIDbPjI/8a3Edr+PiOpcsB7yo8NdW7aHWVaoR/pcDldunMvnnwbk/auPGqmKeAdxtZl7MHX/QmPwhvQ=="], + + "p-map": ["p-map@7.0.4", "", {}, "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ=="], + + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + + "pacote": ["pacote@21.0.3", "", { "dependencies": { "@npmcli/git": "^7.0.0", "@npmcli/installed-package-contents": "^3.0.0", "@npmcli/package-json": "^7.0.0", "@npmcli/promise-spawn": "^8.0.0", "@npmcli/run-script": "^10.0.0", "cacache": "^20.0.0", "fs-minipass": "^3.0.0", "minipass": "^7.0.2", "npm-package-arg": "^13.0.0", "npm-packlist": "^10.0.1", "npm-pick-manifest": "^11.0.1", "npm-registry-fetch": "^19.0.0", "proc-log": "^5.0.0", "promise-retry": "^2.0.1", "sigstore": "^4.0.0", "ssri": "^12.0.0", "tar": "^7.4.3" }, "bin": "bin/index.js" }, "sha512-itdFlanxO0nmQv4ORsvA9K1wv40IPfB9OmWqfaJWvoJ30VKyHsqNgDVeG+TVhI7Gk7XW8slUy7cA9r6dF5qohw=="], + + "parse5": ["parse5@8.0.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA=="], + + "parse5-html-rewriting-stream": ["parse5-html-rewriting-stream@8.0.0", "", { "dependencies": { "entities": "^6.0.0", "parse5": "^8.0.0", "parse5-sax-parser": "^8.0.0" } }, "sha512-wzh11mj8KKkno1pZEu+l2EVeWsuKDfR5KNWZOTsslfUX8lPDZx77m9T0kIoAVkFtD1nx6YF8oh4BnPHvxMtNMw=="], + + "parse5-sax-parser": ["parse5-sax-parser@8.0.0", "", { "dependencies": { "parse5": "^8.0.0" } }, "sha512-/dQ8UzHZwnrzs3EvDj6IkKrD/jIZyTlB+8XrHJvcjNgRdmWruNdN9i9RK/JtxakmlUdPwKubKPTCqvbTgzGhrw=="], + + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "path-scurry": ["path-scurry@2.0.1", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA=="], + + "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "piscina": ["piscina@5.1.3", "", { "optionalDependencies": { "@napi-rs/nice": "^1.0.4" } }, "sha512-0u3N7H4+hbr40KjuVn2uNhOcthu/9usKhnw5vT3J7ply79v3D3M8naI00el9Klcy16x557VsEkkUQaHCWFXC/g=="], + + "pkce-challenge": ["pkce-challenge@5.0.0", "", {}, "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ=="], + + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + + "postcss-media-query-parser": ["postcss-media-query-parser@0.2.3", "", {}, "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig=="], + + "proc-log": ["proc-log@5.0.0", "", {}, "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ=="], + + "promise-retry": ["promise-retry@2.0.1", "", { "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" } }, "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g=="], + + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], + + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "raw-body": ["raw-body@3.0.1", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.7.0", "unpipe": "1.0.0" } }, "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA=="], + + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + + "reflect-metadata": ["reflect-metadata@0.2.2", "", {}, "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], + + "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], + + "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], + + "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], + + "rolldown": ["rolldown@1.0.0-beta.47", "", { "dependencies": { "@oxc-project/types": "=0.96.0", "@rolldown/pluginutils": "1.0.0-beta.47" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-beta.47", "@rolldown/binding-darwin-arm64": "1.0.0-beta.47", "@rolldown/binding-darwin-x64": "1.0.0-beta.47", "@rolldown/binding-freebsd-x64": "1.0.0-beta.47", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.47", "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.47", "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.47", "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.47", "@rolldown/binding-linux-x64-musl": "1.0.0-beta.47", "@rolldown/binding-openharmony-arm64": "1.0.0-beta.47", "@rolldown/binding-wasm32-wasi": "1.0.0-beta.47", "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.47", "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.47", "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.47" }, "bin": "bin/cli.mjs" }, "sha512-Mid74GckX1OeFAOYz9KuXeWYhq3xkXbMziYIC+ULVdUzPTG9y70OBSBQDQn9hQP8u/AfhuYw1R0BSg15nBI4Dg=="], + + "rollup": ["rollup@4.53.3", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.53.3", "@rollup/rollup-android-arm64": "4.53.3", "@rollup/rollup-darwin-arm64": "4.53.3", "@rollup/rollup-darwin-x64": "4.53.3", "@rollup/rollup-freebsd-arm64": "4.53.3", "@rollup/rollup-freebsd-x64": "4.53.3", "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", "@rollup/rollup-linux-arm-musleabihf": "4.53.3", "@rollup/rollup-linux-arm64-gnu": "4.53.3", "@rollup/rollup-linux-arm64-musl": "4.53.3", "@rollup/rollup-linux-loong64-gnu": "4.53.3", "@rollup/rollup-linux-ppc64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-musl": "4.53.3", "@rollup/rollup-linux-s390x-gnu": "4.53.3", "@rollup/rollup-linux-x64-gnu": "4.53.3", "@rollup/rollup-linux-x64-musl": "4.53.3", "@rollup/rollup-openharmony-arm64": "4.53.3", "@rollup/rollup-win32-arm64-msvc": "4.53.3", "@rollup/rollup-win32-ia32-msvc": "4.53.3", "@rollup/rollup-win32-x64-gnu": "4.53.3", "@rollup/rollup-win32-x64-msvc": "4.53.3", "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA=="], + + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + + "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "sass": ["sass@1.93.2", "", { "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", "source-map-js": ">=0.6.2 <2.0.0" }, "optionalDependencies": { "@parcel/watcher": "^2.4.1" }, "bin": "sass.js" }, "sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg=="], + + "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], + + "semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + + "send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="], + + "serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "sigstore": ["sigstore@4.0.0", "", { "dependencies": { "@sigstore/bundle": "^4.0.0", "@sigstore/core": "^3.0.0", "@sigstore/protobuf-specs": "^0.5.0", "@sigstore/sign": "^4.0.0", "@sigstore/tuf": "^4.0.0", "@sigstore/verify": "^3.0.0" } }, "sha512-Gw/FgHtrLM9WP8P5lLcSGh9OQcrTruWCELAiS48ik1QbL0cH+dfjomiRTUE9zzz+D1N6rOLkwXUvVmXZAsNE0Q=="], + + "slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="], + + "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], + + "socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="], + + "socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="], + + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + + "spdx-correct": ["spdx-correct@3.2.0", "", { "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" } }, "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA=="], + + "spdx-exceptions": ["spdx-exceptions@2.5.0", "", {}, "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w=="], + + "spdx-expression-parse": ["spdx-expression-parse@3.0.1", "", { "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q=="], + + "spdx-license-ids": ["spdx-license-ids@3.0.22", "", {}, "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ=="], + + "ssri": ["ssri@12.0.0", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ=="], + + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + + "stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="], + + "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + + "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], + + "tailwindcss": ["tailwindcss@4.1.17", "", {}, "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q=="], + + "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], + + "tar": ["tar@7.5.2", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg=="], + + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="], + + "tldts": ["tldts@7.0.18", "", { "dependencies": { "tldts-core": "^7.0.18" }, "bin": "bin/cli.js" }, "sha512-lCcgTAgMxQ1JKOWrVGo6E69Ukbnx4Gc1wiYLRf6J5NN4HRYJtCby1rPF8rkQ4a6qqoFBK5dvjJ1zJ0F7VfDSvw=="], + + "tldts-core": ["tldts-core@7.0.18", "", {}, "sha512-jqJC13oP4FFAahv4JT/0WTDrCF9Okv7lpKtOZUGPLiAnNbACcSg8Y8T+Z9xthOmRBqi/Sob4yi0TE0miRCvF7Q=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + + "tough-cookie": ["tough-cookie@6.0.0", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w=="], + + "tr46": ["tr46@6.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "tuf-js": ["tuf-js@4.0.0", "", { "dependencies": { "@tufjs/models": "4.0.0", "debug": "^4.4.1", "make-fetch-happen": "^15.0.0" } }, "sha512-Lq7ieeGvXDXwpoSmOSgLWVdsGGV9J4a77oDTAPe/Ltrqnnm/ETaRlBAQTH5JatEh8KXuE6sddf9qAv1Q2282Hg=="], + + "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici": ["undici@7.16.0", "", {}, "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g=="], + + "unique-filename": ["unique-filename@4.0.0", "", { "dependencies": { "unique-slug": "^5.0.0" } }, "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ=="], + + "unique-slug": ["unique-slug@5.0.0", "", { "dependencies": { "imurmurhash": "^0.1.4" } }, "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg=="], + + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "validate-npm-package-license": ["validate-npm-package-license@3.0.4", "", { "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" } }, "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew=="], + + "validate-npm-package-name": ["validate-npm-package-name@6.0.2", "", {}, "sha512-IUoow1YUtvoBBC06dXs8bR8B9vuA3aJfmQNKMoaPG/OFsPmoQvw8xh+6Ye25Gx9DQhoEom3Pcu9MKHerm/NpUQ=="], + + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "vite": ["vite@7.2.2", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "less", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": "bin/vite.js" }, "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ=="], + + "vitest": ["vitest@4.0.12", "", { "dependencies": { "@vitest/expect": "4.0.12", "@vitest/mocker": "4.0.12", "@vitest/pretty-format": "4.0.12", "@vitest/runner": "4.0.12", "@vitest/snapshot": "4.0.12", "@vitest/spy": "4.0.12", "@vitest/utils": "4.0.12", "debug": "^4.4.3", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/debug": "^4.1.12", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.12", "@vitest/browser-preview": "4.0.12", "@vitest/browser-webdriverio": "4.0.12", "@vitest/ui": "4.0.12", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/debug", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom"], "bin": "vitest.mjs" }, "sha512-pmW4GCKQ8t5Ko1jYjC3SqOr7TUKN7uHOHB/XGsAIb69eYu6d1ionGSsb5H9chmPf+WeXt0VE7jTXsB1IvWoNbw=="], + + "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], + + "watchpack": ["watchpack@2.4.4", "", { "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" } }, "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA=="], + + "weak-lru-cache": ["weak-lru-cache@1.2.2", "", {}, "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw=="], + + "webidl-conversions": ["webidl-conversions@8.0.0", "", {}, "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA=="], + + "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], + + "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], + + "whatwg-url": ["whatwg-url@15.1.0", "", { "dependencies": { "tr46": "^6.0.0", "webidl-conversions": "^8.0.0" } }, "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": "cli.js" }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + + "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], + + "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], + + "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], + + "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + + "yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="], + + "yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], + + "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], + + "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="], + + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "zod-to-json-schema": ["zod-to-json-schema@3.25.0", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ=="], + + "@angular-devkit/core/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + + "@asamuzakjp/css-color/lru-cache": ["lru-cache@11.2.2", "", {}, "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg=="], + + "@babel/core/convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "@babel/core/semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], + + "@inquirer/external-editor/iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="], + + "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + + "@modelcontextprotocol/sdk/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + + "@npmcli/agent/lru-cache": ["lru-cache@11.2.2", "", {}, "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg=="], + + "@npmcli/git/@npmcli/promise-spawn": ["@npmcli/promise-spawn@9.0.1", "", { "dependencies": { "which": "^6.0.0" } }, "sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q=="], + + "@npmcli/git/ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], + + "@npmcli/git/lru-cache": ["lru-cache@11.2.2", "", {}, "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg=="], + + "@npmcli/git/proc-log": ["proc-log@6.0.0", "", {}, "sha512-KG/XsTDN901PNfPfAMmj6N/Ywg9tM+bHK8pAz+27fS4N4Pcr+4zoYBOcGSBu6ceXYNPxkLpa4ohtfxV1XcLAfA=="], + + "@npmcli/git/which": ["which@6.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg=="], + + "@npmcli/package-json/proc-log": ["proc-log@6.0.0", "", {}, "sha512-KG/XsTDN901PNfPfAMmj6N/Ywg9tM+bHK8pAz+27fS4N4Pcr+4zoYBOcGSBu6ceXYNPxkLpa4ohtfxV1XcLAfA=="], + + "@npmcli/promise-spawn/which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], + + "@npmcli/run-script/@npmcli/promise-spawn": ["@npmcli/promise-spawn@9.0.1", "", { "dependencies": { "which": "^6.0.0" } }, "sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q=="], + + "@npmcli/run-script/proc-log": ["proc-log@6.0.0", "", {}, "sha512-KG/XsTDN901PNfPfAMmj6N/Ywg9tM+bHK8pAz+27fS4N4Pcr+4zoYBOcGSBu6ceXYNPxkLpa4ohtfxV1XcLAfA=="], + + "@npmcli/run-script/which": ["which@6.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg=="], + + "@parcel/watcher/detect-libc": ["detect-libc@1.0.3", "", { "bin": "bin/detect-libc.js" }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="], + + "@parcel/watcher/node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], + + "@tailwindcss/node/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "@tufjs/models/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "@vitest/mocker/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "@vitest/snapshot/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "body-parser/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + + "cacache/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": "dist/esm/bin.mjs" }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="], + + "cacache/lru-cache": ["lru-cache@11.2.2", "", {}, "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg=="], + + "cacache/ssri": ["ssri@13.0.0", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-yizwGBpbCn4YomB2lzhZqrHLJoqFGXihNbib3ozhqF/cIp5ue+xSmOQrjNasEE62hFxsCcg/V/z23t4n8jMEng=="], + + "cli-truncate/string-width": ["string-width@8.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg=="], + + "cliui/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "cliui/wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], + + "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + + "encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + + "hosted-git-info/lru-cache": ["lru-cache@11.2.2", "", {}, "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg=="], + + "htmlparser2/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + + "http-errors/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], + + "log-update/wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], + + "make-fetch-happen/proc-log": ["proc-log@6.0.0", "", {}, "sha512-KG/XsTDN901PNfPfAMmj6N/Ywg9tM+bHK8pAz+27fS4N4Pcr+4zoYBOcGSBu6ceXYNPxkLpa4ohtfxV1XcLAfA=="], + + "make-fetch-happen/ssri": ["ssri@13.0.0", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-yizwGBpbCn4YomB2lzhZqrHLJoqFGXihNbib3ozhqF/cIp5ue+xSmOQrjNasEE62hFxsCcg/V/z23t4n8jMEng=="], + + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "minipass-sized/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "node-gyp/proc-log": ["proc-log@6.0.0", "", {}, "sha512-KG/XsTDN901PNfPfAMmj6N/Ywg9tM+bHK8pAz+27fS4N4Pcr+4zoYBOcGSBu6ceXYNPxkLpa4ohtfxV1XcLAfA=="], + + "node-gyp/which": ["which@6.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg=="], + + "npm-packlist/proc-log": ["proc-log@6.0.0", "", {}, "sha512-KG/XsTDN901PNfPfAMmj6N/Ywg9tM+bHK8pAz+27fS4N4Pcr+4zoYBOcGSBu6ceXYNPxkLpa4ohtfxV1XcLAfA=="], + + "npm-pick-manifest/npm-normalize-package-bin": ["npm-normalize-package-bin@5.0.0", "", {}, "sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag=="], + + "npm-registry-fetch/proc-log": ["proc-log@6.0.0", "", {}, "sha512-KG/XsTDN901PNfPfAMmj6N/Ywg9tM+bHK8pAz+27fS4N4Pcr+4zoYBOcGSBu6ceXYNPxkLpa4ohtfxV1XcLAfA=="], + + "ora/string-width": ["string-width@8.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg=="], + + "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + + "path-scurry/lru-cache": ["lru-cache@11.2.2", "", {}, "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg=="], + + "raw-body/iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="], + + "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "string-width-cjs/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": "bin/esbuild" }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + + "vitest/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "@inquirer/core/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "@inquirer/core/wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "@inquirer/core/wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "@npmcli/git/which/isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="], + + "@npmcli/promise-spawn/which/isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="], + + "@npmcli/run-script/which/isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="], + + "cliui/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + + "log-update/wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "minipass-flush/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "minipass-pipeline/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "minipass-sized/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "node-gyp/which/isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="], + + "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + + "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "wrap-ansi-cjs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + + "@inquirer/core/wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "@inquirer/core/wrap-ansi/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "@inquirer/core/wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "log-update/wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + } +} diff --git a/frontend-ng/data-engine/package.json b/frontend-ng/data-engine/package.json index 0f91fb8..802692c 100644 --- a/frontend-ng/data-engine/package.json +++ b/frontend-ng/data-engine/package.json @@ -6,7 +6,10 @@ "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", - "test": "ng test" + "test": "ng test", + "tauri": "tauri", + "tauri:dev": "tauri dev", + "tauri:build": "tauri build" }, "prettier": { "printWidth": 100, @@ -21,8 +24,10 @@ ] }, "private": true, - "packageManager": "npm@11.6.2", + "packageManager": "bun@1.3.6", "dependencies": { + "@tauri-apps/api": "^2.0.0", + "@tauri-apps/plugin-dialog": "^2.0.0", "@angular/common": "^21.0.0", "@angular/compiler": "^21.0.0", "@angular/core": "^21.0.0", @@ -33,6 +38,7 @@ "tslib": "^2.3.0" }, "devDependencies": { + "@tauri-apps/cli": "^2.0.0", "@angular/build": "^21.0.0", "@angular/cli": "^21.0.0", "@angular/compiler-cli": "^21.0.0", @@ -43,4 +49,4 @@ "typescript": "~5.9.2", "vitest": "^4.0.8" } -} \ No newline at end of file +} diff --git a/frontend-ng/data-engine/src-tauri/Cargo.toml b/frontend-ng/data-engine/src-tauri/Cargo.toml new file mode 100644 index 0000000..8a164bd --- /dev/null +++ b/frontend-ng/data-engine/src-tauri/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "data-engine-desktop" +version = "0.0.0" +description = "Desktop wrapper for the Data Engine Angular frontend" +authors = ["Rumarino Team"] +edition = "2021" + +[lib] +name = "data_engine_desktop_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = [] } +tauri-plugin-dialog = "2" + diff --git a/frontend-ng/data-engine/src-tauri/build.rs b/frontend-ng/data-engine/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/frontend-ng/data-engine/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/frontend-ng/data-engine/src-tauri/capabilities/default.json b/frontend-ng/data-engine/src-tauri/capabilities/default.json new file mode 100644 index 0000000..027099f --- /dev/null +++ b/frontend-ng/data-engine/src-tauri/capabilities/default.json @@ -0,0 +1,12 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Default capability for the Data Engine desktop window", + "windows": [ + "main" + ], + "permissions": [ + "core:default", + "dialog:default" + ] +} diff --git a/frontend-ng/data-engine/src-tauri/src/lib.rs b/frontend-ng/data-engine/src-tauri/src/lib.rs new file mode 100644 index 0000000..f41a5ed --- /dev/null +++ b/frontend-ng/data-engine/src-tauri/src/lib.rs @@ -0,0 +1,7 @@ +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .plugin(tauri_plugin_dialog::init()) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/frontend-ng/data-engine/src-tauri/src/main.rs b/frontend-ng/data-engine/src-tauri/src/main.rs new file mode 100644 index 0000000..b09b9da --- /dev/null +++ b/frontend-ng/data-engine/src-tauri/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + data_engine_desktop_lib::run(); +} diff --git a/frontend-ng/data-engine/src-tauri/tauri.conf.json b/frontend-ng/data-engine/src-tauri/tauri.conf.json new file mode 100644 index 0000000..4981b27 --- /dev/null +++ b/frontend-ng/data-engine/src-tauri/tauri.conf.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "Data Engine", + "version": "0.0.0", + "identifier": "com.rumarino.dataengine", + "build": { + "beforeDevCommand": "bun run start -- --host 127.0.0.1 --port 4200", + "devUrl": "http://127.0.0.1:4200", + "beforeBuildCommand": "bun run build", + "frontendDist": "../dist/data-engine/browser" + }, + "app": { + "windows": [ + { + "label": "main", + "title": "Data Engine", + "width": 1600, + "height": 1000, + "minWidth": 1200, + "minHeight": 720, + "resizable": true + } + ] + }, + "bundle": { + "active": true, + "targets": "all" + } +} diff --git a/frontend-ng/data-engine/src/app/app.spec.ts b/frontend-ng/data-engine/src/app/app.spec.ts index 2de8402..08e00bb 100644 --- a/frontend-ng/data-engine/src/app/app.spec.ts +++ b/frontend-ng/data-engine/src/app/app.spec.ts @@ -14,10 +14,10 @@ describe('App', () => { expect(app).toBeTruthy(); }); - it('should render title', async () => { + it('should render the router outlet host', async () => { const fixture = TestBed.createComponent(App); await fixture.whenStable(); const compiled = fixture.nativeElement as HTMLElement; - expect(compiled.querySelector('h1')?.textContent).toContain('Hello, data-engine'); + expect(compiled.querySelector('router-outlet')).toBeTruthy(); }); }); diff --git a/frontend-ng/data-engine/src/app/services/backend.service.spec.ts b/frontend-ng/data-engine/src/app/services/backend.service.spec.ts new file mode 100644 index 0000000..062d8b2 --- /dev/null +++ b/frontend-ng/data-engine/src/app/services/backend.service.spec.ts @@ -0,0 +1,66 @@ +import { HttpClient } from '@angular/common/http'; +import { of } from 'rxjs'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { BackendService } from './backend.service'; + +describe('BackendService', () => { + const originalGlobalApiUrl = (globalThis as { __DATA_ENGINE_API_URL__?: string }).__DATA_ENGINE_API_URL__; + + beforeEach(() => { + localStorage.clear(); + delete (globalThis as { __DATA_ENGINE_API_URL__?: string }).__DATA_ENGINE_API_URL__; + }); + + afterEach(() => { + localStorage.clear(); + if (originalGlobalApiUrl === undefined) { + delete (globalThis as { __DATA_ENGINE_API_URL__?: string }).__DATA_ENGINE_API_URL__; + return; + } + (globalThis as { __DATA_ENGINE_API_URL__?: string }).__DATA_ENGINE_API_URL__ = originalGlobalApiUrl; + }); + + it('defaults to localhost when no override exists', () => { + const http = { post: vi.fn(), get: vi.fn() } as unknown as HttpClient; + + const service = new BackendService(http); + + expect(service.getApiUrl()).toBe('http://127.0.0.1:8000'); + }); + + it('prefers stored API URL over global config', () => { + localStorage.setItem('dataEngineApiUrl', 'http://192.168.0.50:9000/'); + (globalThis as { __DATA_ENGINE_API_URL__?: string }).__DATA_ENGINE_API_URL__ = 'http://10.0.0.1:8000'; + const http = { post: vi.fn(), get: vi.fn() } as unknown as HttpClient; + + const service = new BackendService(http); + + expect(service.getApiUrl()).toBe('http://192.168.0.50:9000'); + }); + + it('updates future requests after applying a new API URL', () => { + const http = { + post: vi.fn(() => of({})), + get: vi.fn(() => of({})), + } as unknown as HttpClient; + const service = new BackendService(http); + + service.setApiUrl('http://localhost:9001/'); + service.getVideoMaskData(3).subscribe(); + + expect(service.getApiUrl()).toBe('http://localhost:9001'); + expect(localStorage.getItem('dataEngineApiUrl')).toBe('http://localhost:9001'); + expect((http.get as any).mock.calls[0][0]).toBe('http://localhost:9001/video/mask_data/3'); + }); + + it('resets the API URL back to localhost and clears persisted override', () => { + const http = { post: vi.fn(), get: vi.fn() } as unknown as HttpClient; + const service = new BackendService(http); + + service.setApiUrl('http://localhost:9001'); + service.resetApiUrl(); + + expect(service.getApiUrl()).toBe('http://127.0.0.1:8000'); + expect(localStorage.getItem('dataEngineApiUrl')).toBeNull(); + }); +}); diff --git a/frontend-ng/data-engine/src/app/services/backend.service.ts b/frontend-ng/data-engine/src/app/services/backend.service.ts index 9173acc..b9500dc 100644 --- a/frontend-ng/data-engine/src/app/services/backend.service.ts +++ b/frontend-ng/data-engine/src/app/services/backend.service.ts @@ -2,6 +2,9 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; +const API_URL_STORAGE_KEY = 'dataEngineApiUrl'; +const DEFAULT_API_URL = 'http://127.0.0.1:8000'; + export interface VideoInitStateRequest { video_frames_dir: string; online_mode?: boolean; @@ -113,21 +116,60 @@ export interface TrackPromptPointsResponse { providedIn: 'root' }) export class BackendService { - private readonly apiUrl = this.resolveApiUrl(); + private apiUrl = this.resolveApiUrl(); constructor(private http: HttpClient) { } private resolveApiUrl(): string { + const persistedConfig = this.readStoredApiUrl(); const globalConfig = (globalThis as { __DATA_ENGINE_API_URL__?: string }).__DATA_ENGINE_API_URL__; - const localStorageConfig = typeof localStorage !== 'undefined' - ? localStorage.getItem('dataEngineApiUrl') - : null; + return this.normalizeApiUrl(persistedConfig || globalConfig || DEFAULT_API_URL); + } + + private readStoredApiUrl(): string | null { + if (typeof localStorage === 'undefined') { + return null; + } + return localStorage.getItem(API_URL_STORAGE_KEY); + } + + private writeStoredApiUrl(value: string | null): void { + if (typeof localStorage === 'undefined') { + return; + } + if (value === null) { + localStorage.removeItem(API_URL_STORAGE_KEY); + return; + } + localStorage.setItem(API_URL_STORAGE_KEY, value); + } + + private normalizeApiUrl(value: string): string { + const normalized = value.trim().replace(/^['"]|['"]$/g, ''); + if (!normalized) { + return DEFAULT_API_URL; + } + return normalized.replace(/\/+$/, ''); + } + + private endpoint(path: string): string { + return `${this.apiUrl}${path}`; + } + + getApiUrl(): string { + return this.apiUrl; + } + + setApiUrl(value: string): string { + this.apiUrl = this.normalizeApiUrl(value); + this.writeStoredApiUrl(this.apiUrl); + return this.apiUrl; + } - const browserHost = typeof window !== 'undefined' && window.location.hostname - ? window.location.hostname - : '127.0.0.1'; - const fallback = `http://${browserHost}:8000`; - return (globalConfig || localStorageConfig || fallback).replace(/\/+$/, ''); + resetApiUrl(): string { + this.apiUrl = DEFAULT_API_URL; + this.writeStoredApiUrl(null); + return this.apiUrl; } private safeDecodeURIComponent(value: string): string { @@ -172,50 +214,50 @@ export class BackendService { video_frames_dir: this.normalizePath(dir), ...options }; - return this.http.post(`${this.apiUrl}/video/init_state`, payload); + return this.http.post(this.endpoint('/video/init_state'), payload); } resetVideoState(): Observable { - return this.http.post(`${this.apiUrl}/video/reset_state`, {}); + return this.http.post(this.endpoint('/video/reset_state'), {}); } addNewPointsOrBox(request: VideoAddPointsOrBoxRequest): Observable { - return this.http.post(`${this.apiUrl}/video/add_new_points_or_box`, request); + return this.http.post(this.endpoint('/video/add_new_points_or_box'), request); } propagateInVideo(request: VideoPropagateRequest): Observable { - return this.http.post(`${this.apiUrl}/video/propagate_in_video`, request); + return this.http.post(this.endpoint('/video/propagate_in_video'), request); } clearAllPromptsInFrame(frameIdx: number, objId: number): Observable { - return this.http.post(`${this.apiUrl}/video/clear_all_prompts_in_frame`, null, { + return this.http.post(this.endpoint('/video/clear_all_prompts_in_frame'), null, { params: { frame_idx: frameIdx.toString(), obj_id: objId.toString() } }); } removeObject(objId: number): Observable { - return this.http.post(`${this.apiUrl}/video/remove_object`, null, { + return this.http.post(this.endpoint('/video/remove_object'), null, { params: { obj_id: objId.toString() } }); } getVideoInfo(): Observable<{ num_frames: number, frame_files: string[] }> { - return this.http.get<{ num_frames: number, frame_files: string[] }>(`${this.apiUrl}/video/info`); + return this.http.get<{ num_frames: number, frame_files: string[] }>(this.endpoint('/video/info')); } getVideoFrameUrl(frameIdx: number): string { - return `${this.apiUrl}/video/frame/${frameIdx}`; + return this.endpoint(`/video/frame/${frameIdx}`); } getVideoMaskFrameUrl(frameIdx: number): string { - return `${this.apiUrl}/video/mask_frame/${frameIdx}`; + return this.endpoint(`/video/mask_frame/${frameIdx}`); } getVideoMaskData(frameIdx: number): Observable { - return this.http.get(`${this.apiUrl}/video/mask_data/${frameIdx}`); + return this.http.get(this.endpoint(`/video/mask_data/${frameIdx}`)); } trackPromptPoints(request: TrackPromptPointsRequest): Observable { - return this.http.post(`${this.apiUrl}/tracking/track_prompt_points`, request); + return this.http.post(this.endpoint('/tracking/track_prompt_points'), request); } } diff --git a/frontend-ng/data-engine/src/app/services/desktop-bridge.service.spec.ts b/frontend-ng/data-engine/src/app/services/desktop-bridge.service.spec.ts new file mode 100644 index 0000000..d39c37d --- /dev/null +++ b/frontend-ng/data-engine/src/app/services/desktop-bridge.service.spec.ts @@ -0,0 +1,21 @@ +import { describe, expect, it, vi } from 'vitest'; +import { DesktopBridgeService } from './desktop-bridge.service'; + +vi.mock('@tauri-apps/plugin-dialog', () => ({ + open: vi.fn(), +})); + +describe('DesktopBridgeService', () => { + it('returns false for browser environments without Tauri globals', () => { + const service = new DesktopBridgeService(); + + expect(service.isTauri()).toBe(false); + }); + + it('returns null without throwing when native pickers are unavailable', async () => { + const service = new DesktopBridgeService(); + + await expect(service.pickVideoFile()).resolves.toBeNull(); + await expect(service.pickFramesDirectory()).resolves.toBeNull(); + }); +}); diff --git a/frontend-ng/data-engine/src/app/services/desktop-bridge.service.ts b/frontend-ng/data-engine/src/app/services/desktop-bridge.service.ts new file mode 100644 index 0000000..73d57e0 --- /dev/null +++ b/frontend-ng/data-engine/src/app/services/desktop-bridge.service.ts @@ -0,0 +1,75 @@ +import { Injectable } from '@angular/core'; + +const SUPPORTED_VIDEO_EXTENSIONS = ['mp4', 'mov', 'avi', 'mkv', 'webm', 'm4v']; + +@Injectable({ + providedIn: 'root' +}) +export class DesktopBridgeService { + isTauri(): boolean { + if (typeof window === 'undefined') { + return false; + } + return '__TAURI_INTERNALS__' in window || '__TAURI__' in window; + } + + async pickVideoFile(): Promise { + return this.openPathPicker({ + multiple: false, + directory: false, + filters: [ + { + name: 'Video', + extensions: SUPPORTED_VIDEO_EXTENSIONS, + }, + ], + }); + } + + async pickFramesDirectory(): Promise { + return this.openPathPicker({ + multiple: false, + directory: true, + }); + } + + private async openPathPicker(options: Record): Promise { + if (!this.isTauri()) { + return null; + } + + try { + const dialog = await import('@tauri-apps/plugin-dialog'); + const selected = await dialog.open(options); + if (typeof selected !== 'string' || !selected.trim()) { + return null; + } + return this.normalizeNativePath(selected); + } catch (error) { + console.error('Failed to open native picker', error); + return null; + } + } + + private normalizeNativePath(value: string): string { + let normalized = value.trim(); + if (!normalized.startsWith('file://')) { + return normalized; + } + + try { + const parsed = new URL(normalized); + normalized = decodeURIComponent(parsed.pathname || ''); + if (parsed.host && parsed.host !== 'localhost') { + normalized = `//${parsed.host}${normalized}`; + } + if (/^\/[A-Za-z]:\//.test(normalized)) { + normalized = normalized.slice(1); + } + } catch { + normalized = normalized.replace(/^file:\/\//, ''); + } + + return normalized; + } +} diff --git a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.css b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.css index 8c39624..22838a7 100644 --- a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.css +++ b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.css @@ -21,6 +21,18 @@ padding: 5px; } +.api-url-controls { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; +} + +.api-url-input { + flex: 0 1 260px; + min-width: 220px; +} + .main-area { flex-grow: 1; display: flex; @@ -215,3 +227,13 @@ button:disabled { transform: rotate(360deg); } } + +@media (max-width: 960px) { + .api-url-controls { + width: 100%; + } + + .api-url-input { + flex: 1 1 220px; + } +} diff --git a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.html b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.html index 1c22036..9526491 100644 --- a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.html +++ b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.html @@ -2,9 +2,21 @@
- - + + +
+ + + +
{ let component: VideoMaskerComponent; let backendMock: { addNewPointsOrBox: ReturnType; + getApiUrl: ReturnType; + setApiUrl: ReturnType; + resetApiUrl: ReturnType; + }; + let desktopBridgeMock: { + isTauri: ReturnType; + pickVideoFile: ReturnType; + pickFramesDirectory: ReturnType; }; const makeResponse = (overrides: Partial): VideoAddPointsResponse => ({ @@ -25,6 +34,14 @@ describe('VideoMaskerComponent sync contract', () => { beforeEach(async () => { backendMock = { addNewPointsOrBox: vi.fn(), + getApiUrl: vi.fn(() => 'http://127.0.0.1:8000'), + setApiUrl: vi.fn((value: string) => value), + resetApiUrl: vi.fn(() => 'http://127.0.0.1:8000'), + }; + desktopBridgeMock = { + isTauri: vi.fn(() => false), + pickVideoFile: vi.fn(), + pickFramesDirectory: vi.fn(), }; await TestBed.configureTestingModule({ @@ -34,8 +51,15 @@ describe('VideoMaskerComponent sync contract', () => { provide: BackendService, useValue: { addNewPointsOrBox: backendMock.addNewPointsOrBox, + getApiUrl: backendMock.getApiUrl, + setApiUrl: backendMock.setApiUrl, + resetApiUrl: backendMock.resetApiUrl, }, }, + { + provide: DesktopBridgeService, + useValue: desktopBridgeMock, + }, ], }).compileComponents(); @@ -102,4 +126,22 @@ describe('VideoMaskerComponent sync contract', () => { expect(component.liveEditedObjectFrames().get(5)?.has(1)).toBe(true); expect(component.lastMaskPixelCount()).toBe(0); }); + + it('uses the native Tauri video picker when desktop runtime is available', async () => { + desktopBridgeMock.isTauri.mockReturnValue(true); + desktopBridgeMock.pickVideoFile.mockResolvedValue('C:/videos/example.mp4'); + + await component.browseVideo(); + + expect(desktopBridgeMock.pickVideoFile).toHaveBeenCalled(); + expect(component.videoDir()).toBe('C:/videos/example.mp4'); + }); + + it('falls back to browser file input when Tauri runtime is unavailable', async () => { + const pickerSpy = vi.spyOn(component, 'openVideoFilePicker').mockImplementation(() => undefined); + + await component.browseVideo(); + + expect(pickerSpy).toHaveBeenCalled(); + }); }); diff --git a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.ts b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.ts index 264162b..bbb33f5 100644 --- a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.ts +++ b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.ts @@ -8,6 +8,7 @@ import { VideoAddPointsOrBoxRequest, VideoMaskObjectData } from '../services/backend.service'; +import { DesktopBridgeService } from '../services/desktop-bridge.service'; interface MaskObject { id: number; @@ -42,6 +43,7 @@ export class VideoMaskerComponent { @ViewChild('framesDirInput') framesDirInputRef?: ElementRef; videoDir = signal(''); + apiUrlInput = signal(''); isInitialized = signal(false); numFrames = signal(0); targetFrameIdx = signal(0); @@ -79,7 +81,12 @@ export class VideoMaskerComponent { private currentBaseImage: HTMLImageElement | null = null; private currentMaskObjects: { [objId: string]: VideoMaskObjectData } = {}; - constructor(private backend: BackendService) { + constructor( + private backend: BackendService, + private desktopBridge: DesktopBridgeService, + ) { + this.apiUrlInput.set(this.backend.getApiUrl()); + effect(() => { if (this.isInitialized()) { this.loadFrame(this.targetFrameIdx()); @@ -181,6 +188,40 @@ export class VideoMaskerComponent { this.videoDir.set(value); } + onApiUrlChange(value: string) { + this.apiUrlInput.set(value); + } + + applyApiUrl() { + this.apiUrlInput.set(this.backend.setApiUrl(this.apiUrlInput())); + } + + resetApiUrl() { + this.apiUrlInput.set(this.backend.resetApiUrl()); + } + + async browseVideo() { + if (this.desktopBridge.isTauri()) { + const selectedPath = await this.desktopBridge.pickVideoFile(); + if (selectedPath) { + this.videoDir.set(selectedPath); + } + return; + } + this.openVideoFilePicker(); + } + + async browseFramesDirectory() { + if (this.desktopBridge.isTauri()) { + const selectedPath = await this.desktopBridge.pickFramesDirectory(); + if (selectedPath) { + this.videoDir.set(selectedPath); + } + return; + } + this.openFramesDirPicker(); + } + onVideoFileSelected(event: Event) { const input = event.target as HTMLInputElement; const file = input.files?.[0]; @@ -241,6 +282,10 @@ export class VideoMaskerComponent { alert('Selected folder contents are available, but this browser does not expose the full local directory path. Please paste the full frames directory path manually.'); } + isApiUrlDirty(): boolean { + return this.apiUrlInput().trim() !== this.backend.getApiUrl(); + } + async initVideo() { const enteredPath = this.videoDir().trim().replace(/^['\"]|['\"]$/g, ''); if (!enteredPath) { diff --git a/sam2 b/sam2 new file mode 160000 index 0000000..2b90b9f --- /dev/null +++ b/sam2 @@ -0,0 +1 @@ +Subproject commit 2b90b9f5ceec907a1c18123530e92e794ad901a4 From 979ddb283729a39bac664d36095679394c78bcd9 Mon Sep 17 00:00:00 2001 From: "Rafael A." <157764758+HarenDev@users.noreply.github.com> Date: Mon, 27 Apr 2026 21:20:07 -0400 Subject: [PATCH 15/30] config and readme updates --- README.md | 10 +- frontend-ng/data-engine/README.md | 5 +- frontend-ng/data-engine/package.json | 5 +- frontend-ng/data-engine/src-tauri/Cargo.lock | 5050 +++++++++++++++++ .../src-tauri/gen/schemas/acl-manifests.json | 1 + .../src-tauri/gen/schemas/capabilities.json | 1 + .../src-tauri/gen/schemas/desktop-schema.json | 2310 ++++++++ .../src-tauri/gen/schemas/windows-schema.json | 2310 ++++++++ .../data-engine/src-tauri/icons/icon.ico | Bin 0 -> 15086 bytes .../data-engine/src-tauri/tauri.conf.json | 5 +- .../data-engine/src-tauri/tauri.npm.conf.json | 32 + 11 files changed, 9720 insertions(+), 9 deletions(-) create mode 100644 frontend-ng/data-engine/src-tauri/Cargo.lock create mode 100644 frontend-ng/data-engine/src-tauri/gen/schemas/acl-manifests.json create mode 100644 frontend-ng/data-engine/src-tauri/gen/schemas/capabilities.json create mode 100644 frontend-ng/data-engine/src-tauri/gen/schemas/desktop-schema.json create mode 100644 frontend-ng/data-engine/src-tauri/gen/schemas/windows-schema.json create mode 100644 frontend-ng/data-engine/src-tauri/icons/icon.ico create mode 100644 frontend-ng/data-engine/src-tauri/tauri.npm.conf.json diff --git a/README.md b/README.md index 70ec3a4..370a5bd 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,8 @@ A complete data annotation and tracking system with both Python (FastAPI) backen ## Angular Web Frontend 1. Change into `frontend-ng/data-engine` -2. Install dependencies with `bun install` -3. Start the frontend with `bun run start` +2. Install dependencies with `bun install` or `npm install` +3. Start the frontend with `bun run start` or `npm run start` 4. Keep the Python backend running separately The frontend defaults to `http://127.0.0.1:8000` for backend requests and also exposes a compact API URL override in the top bar. @@ -30,11 +30,11 @@ The frontend defaults to `http://127.0.0.1:8000` for backend requests and also e ## Tauri Desktop Frontend 1. Change into `frontend-ng/data-engine` -2. Install JavaScript dependencies with `bun install` +2. Install JavaScript dependencies with `bun install` or `npm install` 3. Install the Rust toolchain and Tauri prerequisites for your OS 4. Start the Python backend separately -5. Run `bun run tauri:dev` for the desktop dev workflow -6. Run `bun run tauri:build` to produce a packaged desktop build +5. Run `bun run tauri:dev` or `npm run tauri:dev:npm` for the desktop dev workflow +6. Run `bun run tauri:build` or `npm run tauri:build:npm` to produce a packaged desktop build The Tauri app only wraps the Angular frontend. It does not bundle or launch the Python backend. diff --git a/frontend-ng/data-engine/README.md b/frontend-ng/data-engine/README.md index d7c2e87..d7e0de8 100644 --- a/frontend-ng/data-engine/README.md +++ b/frontend-ng/data-engine/README.md @@ -18,10 +18,11 @@ The frontend expects the FastAPI backend to be running separately and defaults t The same Angular app can also run as a Tauri desktop application. -1. Install Bun dependencies: +1. Install dependencies with either Bun or npm: ```bash bun install +npm install ``` 2. Start the backend separately. @@ -30,12 +31,14 @@ bun install ```bash bun run tauri:dev +npm run tauri:dev:npm ``` 4. Build a packaged desktop app: ```bash bun run tauri:build +npm run tauri:build:npm ``` The desktop build adds native file and directory pickers, but it still talks to the same HTTP backend API. diff --git a/frontend-ng/data-engine/package.json b/frontend-ng/data-engine/package.json index 802692c..09c1c3f 100644 --- a/frontend-ng/data-engine/package.json +++ b/frontend-ng/data-engine/package.json @@ -9,7 +9,9 @@ "test": "ng test", "tauri": "tauri", "tauri:dev": "tauri dev", - "tauri:build": "tauri build" + "tauri:build": "tauri build", + "tauri:dev:npm": "tauri dev -c src-tauri/tauri.npm.conf.json", + "tauri:build:npm": "tauri build -c src-tauri/tauri.npm.conf.json" }, "prettier": { "printWidth": 100, @@ -24,7 +26,6 @@ ] }, "private": true, - "packageManager": "bun@1.3.6", "dependencies": { "@tauri-apps/api": "^2.0.0", "@tauri-apps/plugin-dialog": "^2.0.0", diff --git a/frontend-ng/data-engine/src-tauri/Cargo.lock b/frontend-ng/data-engine/src-tauri/Cargo.lock new file mode 100644 index 0000000..6780eee --- /dev/null +++ b/frontend-ng/data-engine/src-tauri/Cargo.lock @@ -0,0 +1,5050 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.11.1", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.2.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.2.1", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.11.1", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.11.1", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.29.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "matches", + "phf 0.10.1", + "proc-macro2", + "quote", + "smallvec", + "syn 1.0.109", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf 0.13.1", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "data-engine-desktop" +version = "0.0.0" +dependencies = [ + "tauri", + "tauri-build", + "tauri-plugin-dialog", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.1", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser 0.36.0", + "foldhash 0.2.0", + "html5ever 0.38.0", + "precomputed-hash", + "selectors 0.36.1", + "tendril 0.5.0", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31a88c8d26de40ed18fe748c547845aa39de1db3afd958f8cb91579f3644bcb" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.2+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.11.1", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" +dependencies = [ + "log", + "mac", + "markup5ever 0.14.1", + "match_token", +] + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever 0.38.0", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "js-sys" +version = "0.3.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.11.1", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "kuchikiki" +version = "0.8.8-speedreader" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" +dependencies = [ + "cssparser 0.29.6", + "html5ever 0.29.1", + "indexmap 2.14.0", + "selectors 0.24.0", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "libc", +] + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "markup5ever" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" +dependencies = [ + "log", + "phf 0.11.3", + "phf_codegen 0.11.3", + "string_cache 0.8.9", + "string_cache_codegen 0.5.4", + "tendril 0.4.3", +] + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril 0.5.0", + "web_atoms", +] + +[[package]] +name = "match_token" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c9fec5a4e89860383d778d10563a605838f8f0b2f9303868937e5ff32e86177" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png", + "serde", + "thiserror 2.0.18", + "windows-sys 0.60.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.11.1", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.11.1", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.11.1", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.1", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_shared 0.8.0", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_macros 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros 0.13.1", + "phf_shared 0.13.1", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.6", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.6", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_macros" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.2", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher 1.0.2", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plist" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" +dependencies = [ + "base64 0.22.1", + "indexmap 2.14.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.11+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "selectors" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" +dependencies = [ + "bitflags 1.3.2", + "cssparser 0.29.6", + "derive_more 0.99.20", + "fxhash", + "log", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc 0.2.0", + "smallvec", +] + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.11.1", + "cssparser 0.36.0", + "derive_more 2.1.1", + "log", + "new_debug_unreachable", + "phf 0.13.1", + "phf_codegen 0.13.1", + "precomputed-hash", + "rustc-hash", + "servo_arc 0.4.3", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "servo_arc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.11.3", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.13.1", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.34.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9103edf55f2da3c82aea4c7fab7c4241032bfeea0e71fa557d98e00e7ce7cc20" +dependencies = [ + "bitflags 2.11.1", + "block2", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "once_cell", + "parking_lot", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da77cc00fb9028caf5b5d4650f75e31f1ef3693459dfca7f7e506d1ecef0ba2d" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.18", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bbc990d1dbf57a8e1c7fa2327f2a614d8b757805603c1b9ba5c81bade09fd4d" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "toml 0.9.12+spec-1.1.0", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a24476afd977c5d5d169f72425868613d82747916dd29e0a357c84c4bd6d29" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.117", + "tauri-utils", + "thiserror 2.0.18", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d39b349a98dadaffebb73f0a40dcd1f23c999211e5a2e744403db384d0c33de7" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddde7d51c907b940fb573006cdda9a642d6a7c8153657e88f8a5c3c9290cd4aa" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "toml 0.9.12+spec-1.1.0", + "walkdir", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1fa4150c95ae391946cc8b8f905ab14797427caba3a8a2f79628e956da91809" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.18", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36e1ec28b79f3d0683f4507e1615c36292c0ea6716668770d4396b9b39871ed8" +dependencies = [ + "anyhow", + "dunce", + "glob", + "log", + "objc2-foundation", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.18", + "toml 0.9.12+spec-1.1.0", + "url", +] + +[[package]] +name = "tauri-runtime" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2826d79a3297ed08cd6ea7f412644ef58e32969504bc4fbd8d7dbeabc4445ea2" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11ea2e6f801d275fdd890d6c9603736012742a1c33b96d0db788c9cdebf7f9e" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219a1f983a2af3653f75b5747f76733b0da7ff03069c7a41901a5eb3ace4557d" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dunce", + "glob", + "html5ever 0.29.1", + "http", + "infer", + "json-patch", + "kuchikiki", + "log", + "memchr", + "phf 0.11.3", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.18", + "toml 0.9.12+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.2+spec-1.1.0", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "tendril" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +dependencies = [ + "new_debug_unreachable", + "utf-8", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.52.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.2", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.2", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.2", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.11.1", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png", + "serde", + "thiserror 2.0.18", + "windows-sys 0.60.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.14.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" +dependencies = [ + "phf 0.13.1", + "phf_codegen 0.13.1", + "string_cache 0.9.0", + "string_cache_codegen 0.6.1", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.18", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.14.0", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap 2.14.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.14.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wry" +version = "0.54.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a8135d8676225e5744de000d4dff5a082501bf7db6a1c1495034f8c314edbc" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/frontend-ng/data-engine/src-tauri/gen/schemas/acl-manifests.json b/frontend-ng/data-engine/src-tauri/gen/schemas/acl-manifests.json new file mode 100644 index 0000000..116d3af --- /dev/null +++ b/frontend-ng/data-engine/src-tauri/gen/schemas/acl-manifests.json @@ -0,0 +1 @@ +{"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"]},"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-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-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-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-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-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-internal-toggle-maximize"]},"permissions":{"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-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-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-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/frontend-ng/data-engine/src-tauri/gen/schemas/capabilities.json b/frontend-ng/data-engine/src-tauri/gen/schemas/capabilities.json new file mode 100644 index 0000000..1b7e972 --- /dev/null +++ b/frontend-ng/data-engine/src-tauri/gen/schemas/capabilities.json @@ -0,0 +1 @@ +{"default":{"identifier":"default","description":"Default capability for the Data Engine desktop window","local":true,"windows":["main"],"permissions":["core:default","dialog:default"]}} \ No newline at end of file diff --git a/frontend-ng/data-engine/src-tauri/gen/schemas/desktop-schema.json b/frontend-ng/data-engine/src-tauri/gen/schemas/desktop-schema.json new file mode 100644 index 0000000..a1f4665 --- /dev/null +++ b/frontend-ng/data-engine/src-tauri/gen/schemas/desktop-schema.json @@ -0,0 +1,2310 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CapabilityFile", + "description": "Capability formats accepted in a capability file.", + "anyOf": [ + { + "description": "A single capability.", + "allOf": [ + { + "$ref": "#/definitions/Capability" + } + ] + }, + { + "description": "A list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + }, + { + "description": "A list of capabilities.", + "type": "object", + "required": [ + "capabilities" + ], + "properties": { + "capabilities": { + "description": "The list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + } + } + } + ], + "definitions": { + "Capability": { + "description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```", + "type": "object", + "required": [ + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "Identifier of the capability.\n\n## Example\n\n`main-user-files-write`", + "type": "string" + }, + "description": { + "description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.", + "default": "", + "type": "string" + }, + "remote": { + "description": "Configure remote URLs that can use the capability permissions.\n\nThis setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.\n\n:::caution Make sure you understand the security implications of providing remote sources with local system access. :::\n\n## Example\n\n```json { \"urls\": [\"https://*.mydomain.dev\"] } ```", + "anyOf": [ + { + "$ref": "#/definitions/CapabilityRemote" + }, + { + "type": "null" + } + ] + }, + "local": { + "description": "Whether this capability is enabled for local app URLs or not. Defaults to `true`.", + "default": true, + "type": "boolean" + }, + "windows": { + "description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nIf a window label matches any of the patterns in this list, the capability will be enabled on all the webviews of that window, regardless of the value of [`Self::webviews`].\n\nOn multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "webviews": { + "description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThe capability will be enabled on all the webviews whose label matches any of the patterns in this list, regardless of whether the webview's window label matches a pattern in [`Self::windows`].\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "permissions": { + "description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ] ```", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionEntry" + }, + "uniqueItems": true + }, + "platforms": { + "description": "Limit which target platforms this capability applies to.\n\nBy default all platforms are targeted.\n\n## Example\n\n`[\"macOS\",\"windows\"]`", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "CapabilityRemote": { + "description": "Configuration for remote URLs that are associated with the capability.", + "type": "object", + "required": [ + "urls" + ], + "properties": { + "urls": { + "description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n## Examples\n\n- \"https://*.mydomain.dev\": allows subdomains of mydomain.dev - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionEntry": { + "description": "An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`] or an object that references a permission and extends its scope.", + "anyOf": [ + { + "description": "Reference a permission or permission set by identifier.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + { + "description": "Reference a permission or permission set by identifier and extends its scope.", + "type": "object", + "allOf": [ + { + "properties": { + "identifier": { + "description": "Identifier of the permission or permission set.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + } + ], + "required": [ + "identifier" + ] + } + ] + }, + "Identifier": { + "description": "Permission identifier", + "oneOf": [ + { + "description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`", + "type": "string", + "const": "core:default", + "markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`" + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`", + "type": "string", + "const": "core:app:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`" + }, + { + "description": "Enables the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-hide", + "markdownDescription": "Enables the app_hide command without any pre-configured scope." + }, + { + "description": "Enables the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-show", + "markdownDescription": "Enables the app_show command without any pre-configured scope." + }, + { + "description": "Enables the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-bundle-type", + "markdownDescription": "Enables the bundle_type command without any pre-configured scope." + }, + { + "description": "Enables the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-default-window-icon", + "markdownDescription": "Enables the default_window_icon command without any pre-configured scope." + }, + { + "description": "Enables the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-fetch-data-store-identifiers", + "markdownDescription": "Enables the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Enables the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-identifier", + "markdownDescription": "Enables the identifier command without any pre-configured scope." + }, + { + "description": "Enables the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-name", + "markdownDescription": "Enables the name command without any pre-configured scope." + }, + { + "description": "Enables the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-register-listener", + "markdownDescription": "Enables the register_listener command without any pre-configured scope." + }, + { + "description": "Enables the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-data-store", + "markdownDescription": "Enables the remove_data_store command without any pre-configured scope." + }, + { + "description": "Enables the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-listener", + "markdownDescription": "Enables the remove_listener command without any pre-configured scope." + }, + { + "description": "Enables the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-app-theme", + "markdownDescription": "Enables the set_app_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-dock-visibility", + "markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Enables the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-tauri-version", + "markdownDescription": "Enables the tauri_version command without any pre-configured scope." + }, + { + "description": "Enables the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-version", + "markdownDescription": "Enables the version command without any pre-configured scope." + }, + { + "description": "Denies the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-hide", + "markdownDescription": "Denies the app_hide command without any pre-configured scope." + }, + { + "description": "Denies the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-show", + "markdownDescription": "Denies the app_show command without any pre-configured scope." + }, + { + "description": "Denies the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-bundle-type", + "markdownDescription": "Denies the bundle_type command without any pre-configured scope." + }, + { + "description": "Denies the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-default-window-icon", + "markdownDescription": "Denies the default_window_icon command without any pre-configured scope." + }, + { + "description": "Denies the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-fetch-data-store-identifiers", + "markdownDescription": "Denies the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Denies the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-identifier", + "markdownDescription": "Denies the identifier command without any pre-configured scope." + }, + { + "description": "Denies the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-name", + "markdownDescription": "Denies the name command without any pre-configured scope." + }, + { + "description": "Denies the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-register-listener", + "markdownDescription": "Denies the register_listener command without any pre-configured scope." + }, + { + "description": "Denies the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-data-store", + "markdownDescription": "Denies the remove_data_store command without any pre-configured scope." + }, + { + "description": "Denies the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-listener", + "markdownDescription": "Denies the remove_listener command without any pre-configured scope." + }, + { + "description": "Denies the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-app-theme", + "markdownDescription": "Denies the set_app_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-dock-visibility", + "markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Denies the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-tauri-version", + "markdownDescription": "Denies the tauri_version command without any pre-configured scope." + }, + { + "description": "Denies the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-version", + "markdownDescription": "Denies the version command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`", + "type": "string", + "const": "core:event:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`" + }, + { + "description": "Enables the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit", + "markdownDescription": "Enables the emit command without any pre-configured scope." + }, + { + "description": "Enables the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit-to", + "markdownDescription": "Enables the emit_to command without any pre-configured scope." + }, + { + "description": "Enables the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-listen", + "markdownDescription": "Enables the listen command without any pre-configured scope." + }, + { + "description": "Enables the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-unlisten", + "markdownDescription": "Enables the unlisten command without any pre-configured scope." + }, + { + "description": "Denies the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit", + "markdownDescription": "Denies the emit command without any pre-configured scope." + }, + { + "description": "Denies the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit-to", + "markdownDescription": "Denies the emit_to command without any pre-configured scope." + }, + { + "description": "Denies the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-listen", + "markdownDescription": "Denies the listen command without any pre-configured scope." + }, + { + "description": "Denies the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-unlisten", + "markdownDescription": "Denies the unlisten command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`", + "type": "string", + "const": "core:image:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`" + }, + { + "description": "Enables the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-bytes", + "markdownDescription": "Enables the from_bytes command without any pre-configured scope." + }, + { + "description": "Enables the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-path", + "markdownDescription": "Enables the from_path command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-rgba", + "markdownDescription": "Enables the rgba command without any pre-configured scope." + }, + { + "description": "Enables the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-size", + "markdownDescription": "Enables the size command without any pre-configured scope." + }, + { + "description": "Denies the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-bytes", + "markdownDescription": "Denies the from_bytes command without any pre-configured scope." + }, + { + "description": "Denies the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-path", + "markdownDescription": "Denies the from_path command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-rgba", + "markdownDescription": "Denies the rgba command without any pre-configured scope." + }, + { + "description": "Denies the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-size", + "markdownDescription": "Denies the size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`", + "type": "string", + "const": "core:menu:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`" + }, + { + "description": "Enables the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-append", + "markdownDescription": "Enables the append command without any pre-configured scope." + }, + { + "description": "Enables the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-create-default", + "markdownDescription": "Enables the create_default command without any pre-configured scope." + }, + { + "description": "Enables the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-get", + "markdownDescription": "Enables the get command without any pre-configured scope." + }, + { + "description": "Enables the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-insert", + "markdownDescription": "Enables the insert command without any pre-configured scope." + }, + { + "description": "Enables the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-checked", + "markdownDescription": "Enables the is_checked command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-items", + "markdownDescription": "Enables the items command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-popup", + "markdownDescription": "Enables the popup command without any pre-configured scope." + }, + { + "description": "Enables the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-prepend", + "markdownDescription": "Enables the prepend command without any pre-configured scope." + }, + { + "description": "Enables the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove", + "markdownDescription": "Enables the remove command without any pre-configured scope." + }, + { + "description": "Enables the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove-at", + "markdownDescription": "Enables the remove_at command without any pre-configured scope." + }, + { + "description": "Enables the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-accelerator", + "markdownDescription": "Enables the set_accelerator command without any pre-configured scope." + }, + { + "description": "Enables the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-app-menu", + "markdownDescription": "Enables the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-help-menu-for-nsapp", + "markdownDescription": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-window-menu", + "markdownDescription": "Enables the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-windows-menu-for-nsapp", + "markdownDescription": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-checked", + "markdownDescription": "Enables the set_checked command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-text", + "markdownDescription": "Enables the set_text command without any pre-configured scope." + }, + { + "description": "Enables the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-text", + "markdownDescription": "Enables the text command without any pre-configured scope." + }, + { + "description": "Denies the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-append", + "markdownDescription": "Denies the append command without any pre-configured scope." + }, + { + "description": "Denies the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-create-default", + "markdownDescription": "Denies the create_default command without any pre-configured scope." + }, + { + "description": "Denies the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-get", + "markdownDescription": "Denies the get command without any pre-configured scope." + }, + { + "description": "Denies the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-insert", + "markdownDescription": "Denies the insert command without any pre-configured scope." + }, + { + "description": "Denies the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-checked", + "markdownDescription": "Denies the is_checked command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-items", + "markdownDescription": "Denies the items command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-popup", + "markdownDescription": "Denies the popup command without any pre-configured scope." + }, + { + "description": "Denies the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-prepend", + "markdownDescription": "Denies the prepend command without any pre-configured scope." + }, + { + "description": "Denies the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove", + "markdownDescription": "Denies the remove command without any pre-configured scope." + }, + { + "description": "Denies the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove-at", + "markdownDescription": "Denies the remove_at command without any pre-configured scope." + }, + { + "description": "Denies the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-accelerator", + "markdownDescription": "Denies the set_accelerator command without any pre-configured scope." + }, + { + "description": "Denies the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-app-menu", + "markdownDescription": "Denies the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-help-menu-for-nsapp", + "markdownDescription": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-window-menu", + "markdownDescription": "Denies the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-windows-menu-for-nsapp", + "markdownDescription": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-checked", + "markdownDescription": "Denies the set_checked command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-text", + "markdownDescription": "Denies the set_text command without any pre-configured scope." + }, + { + "description": "Denies the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-text", + "markdownDescription": "Denies the text command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`", + "type": "string", + "const": "core:path:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`" + }, + { + "description": "Enables the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-basename", + "markdownDescription": "Enables the basename command without any pre-configured scope." + }, + { + "description": "Enables the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-dirname", + "markdownDescription": "Enables the dirname command without any pre-configured scope." + }, + { + "description": "Enables the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-extname", + "markdownDescription": "Enables the extname command without any pre-configured scope." + }, + { + "description": "Enables the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-is-absolute", + "markdownDescription": "Enables the is_absolute command without any pre-configured scope." + }, + { + "description": "Enables the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-join", + "markdownDescription": "Enables the join command without any pre-configured scope." + }, + { + "description": "Enables the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-normalize", + "markdownDescription": "Enables the normalize command without any pre-configured scope." + }, + { + "description": "Enables the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve", + "markdownDescription": "Enables the resolve command without any pre-configured scope." + }, + { + "description": "Enables the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve-directory", + "markdownDescription": "Enables the resolve_directory command without any pre-configured scope." + }, + { + "description": "Denies the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-basename", + "markdownDescription": "Denies the basename command without any pre-configured scope." + }, + { + "description": "Denies the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-dirname", + "markdownDescription": "Denies the dirname command without any pre-configured scope." + }, + { + "description": "Denies the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-extname", + "markdownDescription": "Denies the extname command without any pre-configured scope." + }, + { + "description": "Denies the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-is-absolute", + "markdownDescription": "Denies the is_absolute command without any pre-configured scope." + }, + { + "description": "Denies the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-join", + "markdownDescription": "Denies the join command without any pre-configured scope." + }, + { + "description": "Denies the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-normalize", + "markdownDescription": "Denies the normalize command without any pre-configured scope." + }, + { + "description": "Denies the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve", + "markdownDescription": "Denies the resolve command without any pre-configured scope." + }, + { + "description": "Denies the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve-directory", + "markdownDescription": "Denies the resolve_directory command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`", + "type": "string", + "const": "core:resources:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`" + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`", + "type": "string", + "const": "core:tray:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`" + }, + { + "description": "Enables the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-get-by-id", + "markdownDescription": "Enables the get_by_id command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-remove-by-id", + "markdownDescription": "Enables the remove_by_id command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon-as-template", + "markdownDescription": "Enables the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Enables the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-menu", + "markdownDescription": "Enables the set_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-show-menu-on-left-click", + "markdownDescription": "Enables the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Enables the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-temp-dir-path", + "markdownDescription": "Enables the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-tooltip", + "markdownDescription": "Enables the set_tooltip command without any pre-configured scope." + }, + { + "description": "Enables the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-visible", + "markdownDescription": "Enables the set_visible command without any pre-configured scope." + }, + { + "description": "Denies the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-get-by-id", + "markdownDescription": "Denies the get_by_id command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-remove-by-id", + "markdownDescription": "Denies the remove_by_id command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon-as-template", + "markdownDescription": "Denies the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Denies the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-menu", + "markdownDescription": "Denies the set_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-show-menu-on-left-click", + "markdownDescription": "Denies the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Denies the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-temp-dir-path", + "markdownDescription": "Denies the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-tooltip", + "markdownDescription": "Denies the set_tooltip command without any pre-configured scope." + }, + { + "description": "Denies the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-visible", + "markdownDescription": "Denies the set_visible command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`", + "type": "string", + "const": "core:webview:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`" + }, + { + "description": "Enables the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-clear-all-browsing-data", + "markdownDescription": "Enables the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Enables the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview", + "markdownDescription": "Enables the create_webview command without any pre-configured scope." + }, + { + "description": "Enables the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview-window", + "markdownDescription": "Enables the create_webview_window command without any pre-configured scope." + }, + { + "description": "Enables the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-get-all-webviews", + "markdownDescription": "Enables the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-internal-toggle-devtools", + "markdownDescription": "Enables the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Enables the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-print", + "markdownDescription": "Enables the print command without any pre-configured scope." + }, + { + "description": "Enables the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-reparent", + "markdownDescription": "Enables the reparent command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-auto-resize", + "markdownDescription": "Enables the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-background-color", + "markdownDescription": "Enables the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-focus", + "markdownDescription": "Enables the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-position", + "markdownDescription": "Enables the set_webview_position command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-size", + "markdownDescription": "Enables the set_webview_size command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-zoom", + "markdownDescription": "Enables the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Enables the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-close", + "markdownDescription": "Enables the webview_close command without any pre-configured scope." + }, + { + "description": "Enables the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-hide", + "markdownDescription": "Enables the webview_hide command without any pre-configured scope." + }, + { + "description": "Enables the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-position", + "markdownDescription": "Enables the webview_position command without any pre-configured scope." + }, + { + "description": "Enables the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-show", + "markdownDescription": "Enables the webview_show command without any pre-configured scope." + }, + { + "description": "Enables the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-size", + "markdownDescription": "Enables the webview_size command without any pre-configured scope." + }, + { + "description": "Denies the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-clear-all-browsing-data", + "markdownDescription": "Denies the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Denies the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview", + "markdownDescription": "Denies the create_webview command without any pre-configured scope." + }, + { + "description": "Denies the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview-window", + "markdownDescription": "Denies the create_webview_window command without any pre-configured scope." + }, + { + "description": "Denies the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-get-all-webviews", + "markdownDescription": "Denies the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-internal-toggle-devtools", + "markdownDescription": "Denies the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Denies the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-print", + "markdownDescription": "Denies the print command without any pre-configured scope." + }, + { + "description": "Denies the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-reparent", + "markdownDescription": "Denies the reparent command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-auto-resize", + "markdownDescription": "Denies the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-background-color", + "markdownDescription": "Denies the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-focus", + "markdownDescription": "Denies the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-position", + "markdownDescription": "Denies the set_webview_position command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-size", + "markdownDescription": "Denies the set_webview_size command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-zoom", + "markdownDescription": "Denies the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Denies the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-close", + "markdownDescription": "Denies the webview_close command without any pre-configured scope." + }, + { + "description": "Denies the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-hide", + "markdownDescription": "Denies the webview_hide command without any pre-configured scope." + }, + { + "description": "Denies the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-position", + "markdownDescription": "Denies the webview_position command without any pre-configured scope." + }, + { + "description": "Denies the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-show", + "markdownDescription": "Denies the webview_show command without any pre-configured scope." + }, + { + "description": "Denies the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-size", + "markdownDescription": "Denies the webview_size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`", + "type": "string", + "const": "core:window:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`" + }, + { + "description": "Enables the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-available-monitors", + "markdownDescription": "Enables the available_monitors command without any pre-configured scope." + }, + { + "description": "Enables the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-center", + "markdownDescription": "Enables the center command without any pre-configured scope." + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Enables the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-create", + "markdownDescription": "Enables the create command without any pre-configured scope." + }, + { + "description": "Enables the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-current-monitor", + "markdownDescription": "Enables the current_monitor command without any pre-configured scope." + }, + { + "description": "Enables the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-cursor-position", + "markdownDescription": "Enables the cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-destroy", + "markdownDescription": "Enables the destroy command without any pre-configured scope." + }, + { + "description": "Enables the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-get-all-windows", + "markdownDescription": "Enables the get_all_windows command without any pre-configured scope." + }, + { + "description": "Enables the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-hide", + "markdownDescription": "Enables the hide command without any pre-configured scope." + }, + { + "description": "Enables the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-position", + "markdownDescription": "Enables the inner_position command without any pre-configured scope." + }, + { + "description": "Enables the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-size", + "markdownDescription": "Enables the inner_size command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-internal-toggle-maximize", + "markdownDescription": "Enables the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-always-on-top", + "markdownDescription": "Enables the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-closable", + "markdownDescription": "Enables the is_closable command without any pre-configured scope." + }, + { + "description": "Enables the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-decorated", + "markdownDescription": "Enables the is_decorated command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-focused", + "markdownDescription": "Enables the is_focused command without any pre-configured scope." + }, + { + "description": "Enables the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-fullscreen", + "markdownDescription": "Enables the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximizable", + "markdownDescription": "Enables the is_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximized", + "markdownDescription": "Enables the is_maximized command without any pre-configured scope." + }, + { + "description": "Enables the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimizable", + "markdownDescription": "Enables the is_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimized", + "markdownDescription": "Enables the is_minimized command without any pre-configured scope." + }, + { + "description": "Enables the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-resizable", + "markdownDescription": "Enables the is_resizable command without any pre-configured scope." + }, + { + "description": "Enables the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-visible", + "markdownDescription": "Enables the is_visible command without any pre-configured scope." + }, + { + "description": "Enables the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-maximize", + "markdownDescription": "Enables the maximize command without any pre-configured scope." + }, + { + "description": "Enables the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-minimize", + "markdownDescription": "Enables the minimize command without any pre-configured scope." + }, + { + "description": "Enables the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-monitor-from-point", + "markdownDescription": "Enables the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Enables the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-position", + "markdownDescription": "Enables the outer_position command without any pre-configured scope." + }, + { + "description": "Enables the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-size", + "markdownDescription": "Enables the outer_size command without any pre-configured scope." + }, + { + "description": "Enables the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-primary-monitor", + "markdownDescription": "Enables the primary_monitor command without any pre-configured scope." + }, + { + "description": "Enables the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-request-user-attention", + "markdownDescription": "Enables the request_user_attention command without any pre-configured scope." + }, + { + "description": "Enables the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-scale-factor", + "markdownDescription": "Enables the scale_factor command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-bottom", + "markdownDescription": "Enables the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-top", + "markdownDescription": "Enables the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-background-color", + "markdownDescription": "Enables the set_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-count", + "markdownDescription": "Enables the set_badge_count command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-label", + "markdownDescription": "Enables the set_badge_label command without any pre-configured scope." + }, + { + "description": "Enables the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-closable", + "markdownDescription": "Enables the set_closable command without any pre-configured scope." + }, + { + "description": "Enables the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-content-protected", + "markdownDescription": "Enables the set_content_protected command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-grab", + "markdownDescription": "Enables the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-icon", + "markdownDescription": "Enables the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-position", + "markdownDescription": "Enables the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-visible", + "markdownDescription": "Enables the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Enables the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-decorations", + "markdownDescription": "Enables the set_decorations command without any pre-configured scope." + }, + { + "description": "Enables the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-effects", + "markdownDescription": "Enables the set_effects command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focus", + "markdownDescription": "Enables the set_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focusable", + "markdownDescription": "Enables the set_focusable command without any pre-configured scope." + }, + { + "description": "Enables the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-fullscreen", + "markdownDescription": "Enables the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-ignore-cursor-events", + "markdownDescription": "Enables the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Enables the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-max-size", + "markdownDescription": "Enables the set_max_size command without any pre-configured scope." + }, + { + "description": "Enables the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-maximizable", + "markdownDescription": "Enables the set_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-min-size", + "markdownDescription": "Enables the set_min_size command without any pre-configured scope." + }, + { + "description": "Enables the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-minimizable", + "markdownDescription": "Enables the set_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-overlay-icon", + "markdownDescription": "Enables the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-position", + "markdownDescription": "Enables the set_position command without any pre-configured scope." + }, + { + "description": "Enables the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-progress-bar", + "markdownDescription": "Enables the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Enables the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-resizable", + "markdownDescription": "Enables the set_resizable command without any pre-configured scope." + }, + { + "description": "Enables the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-shadow", + "markdownDescription": "Enables the set_shadow command without any pre-configured scope." + }, + { + "description": "Enables the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-simple-fullscreen", + "markdownDescription": "Enables the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size", + "markdownDescription": "Enables the set_size command without any pre-configured scope." + }, + { + "description": "Enables the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size-constraints", + "markdownDescription": "Enables the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Enables the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-skip-taskbar", + "markdownDescription": "Enables the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Enables the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-theme", + "markdownDescription": "Enables the set_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title-bar-style", + "markdownDescription": "Enables the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Enables the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-visible-on-all-workspaces", + "markdownDescription": "Enables the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Enables the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-show", + "markdownDescription": "Enables the show command without any pre-configured scope." + }, + { + "description": "Enables the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-dragging", + "markdownDescription": "Enables the start_dragging command without any pre-configured scope." + }, + { + "description": "Enables the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-resize-dragging", + "markdownDescription": "Enables the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Enables the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-theme", + "markdownDescription": "Enables the theme command without any pre-configured scope." + }, + { + "description": "Enables the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-title", + "markdownDescription": "Enables the title command without any pre-configured scope." + }, + { + "description": "Enables the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-toggle-maximize", + "markdownDescription": "Enables the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unmaximize", + "markdownDescription": "Enables the unmaximize command without any pre-configured scope." + }, + { + "description": "Enables the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unminimize", + "markdownDescription": "Enables the unminimize command without any pre-configured scope." + }, + { + "description": "Denies the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-available-monitors", + "markdownDescription": "Denies the available_monitors command without any pre-configured scope." + }, + { + "description": "Denies the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-center", + "markdownDescription": "Denies the center command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Denies the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-create", + "markdownDescription": "Denies the create command without any pre-configured scope." + }, + { + "description": "Denies the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-current-monitor", + "markdownDescription": "Denies the current_monitor command without any pre-configured scope." + }, + { + "description": "Denies the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-cursor-position", + "markdownDescription": "Denies the cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-destroy", + "markdownDescription": "Denies the destroy command without any pre-configured scope." + }, + { + "description": "Denies the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-get-all-windows", + "markdownDescription": "Denies the get_all_windows command without any pre-configured scope." + }, + { + "description": "Denies the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-hide", + "markdownDescription": "Denies the hide command without any pre-configured scope." + }, + { + "description": "Denies the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-position", + "markdownDescription": "Denies the inner_position command without any pre-configured scope." + }, + { + "description": "Denies the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-size", + "markdownDescription": "Denies the inner_size command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-internal-toggle-maximize", + "markdownDescription": "Denies the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-always-on-top", + "markdownDescription": "Denies the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-closable", + "markdownDescription": "Denies the is_closable command without any pre-configured scope." + }, + { + "description": "Denies the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-decorated", + "markdownDescription": "Denies the is_decorated command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-focused", + "markdownDescription": "Denies the is_focused command without any pre-configured scope." + }, + { + "description": "Denies the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-fullscreen", + "markdownDescription": "Denies the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximizable", + "markdownDescription": "Denies the is_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximized", + "markdownDescription": "Denies the is_maximized command without any pre-configured scope." + }, + { + "description": "Denies the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimizable", + "markdownDescription": "Denies the is_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimized", + "markdownDescription": "Denies the is_minimized command without any pre-configured scope." + }, + { + "description": "Denies the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-resizable", + "markdownDescription": "Denies the is_resizable command without any pre-configured scope." + }, + { + "description": "Denies the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-visible", + "markdownDescription": "Denies the is_visible command without any pre-configured scope." + }, + { + "description": "Denies the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-maximize", + "markdownDescription": "Denies the maximize command without any pre-configured scope." + }, + { + "description": "Denies the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-minimize", + "markdownDescription": "Denies the minimize command without any pre-configured scope." + }, + { + "description": "Denies the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-monitor-from-point", + "markdownDescription": "Denies the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Denies the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-position", + "markdownDescription": "Denies the outer_position command without any pre-configured scope." + }, + { + "description": "Denies the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-size", + "markdownDescription": "Denies the outer_size command without any pre-configured scope." + }, + { + "description": "Denies the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-primary-monitor", + "markdownDescription": "Denies the primary_monitor command without any pre-configured scope." + }, + { + "description": "Denies the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-request-user-attention", + "markdownDescription": "Denies the request_user_attention command without any pre-configured scope." + }, + { + "description": "Denies the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-scale-factor", + "markdownDescription": "Denies the scale_factor command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-bottom", + "markdownDescription": "Denies the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-top", + "markdownDescription": "Denies the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-background-color", + "markdownDescription": "Denies the set_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-count", + "markdownDescription": "Denies the set_badge_count command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-label", + "markdownDescription": "Denies the set_badge_label command without any pre-configured scope." + }, + { + "description": "Denies the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-closable", + "markdownDescription": "Denies the set_closable command without any pre-configured scope." + }, + { + "description": "Denies the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-content-protected", + "markdownDescription": "Denies the set_content_protected command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-grab", + "markdownDescription": "Denies the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-icon", + "markdownDescription": "Denies the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-position", + "markdownDescription": "Denies the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-visible", + "markdownDescription": "Denies the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Denies the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-decorations", + "markdownDescription": "Denies the set_decorations command without any pre-configured scope." + }, + { + "description": "Denies the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-effects", + "markdownDescription": "Denies the set_effects command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focus", + "markdownDescription": "Denies the set_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focusable", + "markdownDescription": "Denies the set_focusable command without any pre-configured scope." + }, + { + "description": "Denies the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-fullscreen", + "markdownDescription": "Denies the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-ignore-cursor-events", + "markdownDescription": "Denies the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Denies the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-max-size", + "markdownDescription": "Denies the set_max_size command without any pre-configured scope." + }, + { + "description": "Denies the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-maximizable", + "markdownDescription": "Denies the set_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-min-size", + "markdownDescription": "Denies the set_min_size command without any pre-configured scope." + }, + { + "description": "Denies the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-minimizable", + "markdownDescription": "Denies the set_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-overlay-icon", + "markdownDescription": "Denies the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-position", + "markdownDescription": "Denies the set_position command without any pre-configured scope." + }, + { + "description": "Denies the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-progress-bar", + "markdownDescription": "Denies the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Denies the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-resizable", + "markdownDescription": "Denies the set_resizable command without any pre-configured scope." + }, + { + "description": "Denies the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-shadow", + "markdownDescription": "Denies the set_shadow command without any pre-configured scope." + }, + { + "description": "Denies the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-simple-fullscreen", + "markdownDescription": "Denies the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size", + "markdownDescription": "Denies the set_size command without any pre-configured scope." + }, + { + "description": "Denies the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size-constraints", + "markdownDescription": "Denies the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Denies the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-skip-taskbar", + "markdownDescription": "Denies the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Denies the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-theme", + "markdownDescription": "Denies the set_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title-bar-style", + "markdownDescription": "Denies the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Denies the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-visible-on-all-workspaces", + "markdownDescription": "Denies the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Denies the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-show", + "markdownDescription": "Denies the show command without any pre-configured scope." + }, + { + "description": "Denies the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-dragging", + "markdownDescription": "Denies the start_dragging command without any pre-configured scope." + }, + { + "description": "Denies the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-resize-dragging", + "markdownDescription": "Denies the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Denies the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-theme", + "markdownDescription": "Denies the theme command without any pre-configured scope." + }, + { + "description": "Denies the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-title", + "markdownDescription": "Denies the title command without any pre-configured scope." + }, + { + "description": "Denies the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-toggle-maximize", + "markdownDescription": "Denies the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unmaximize", + "markdownDescription": "Denies the unmaximize command without any pre-configured scope." + }, + { + "description": "Denies the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unminimize", + "markdownDescription": "Denies the unminimize command without any pre-configured scope." + }, + { + "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\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`", + "type": "string", + "const": "dialog:default", + "markdownDescription": "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\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`" + }, + { + "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)", + "type": "string", + "const": "dialog:allow-ask", + "markdownDescription": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" + }, + { + "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)", + "type": "string", + "const": "dialog:allow-confirm", + "markdownDescription": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" + }, + { + "description": "Enables the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-message", + "markdownDescription": "Enables the message command without any pre-configured scope." + }, + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-open", + "markdownDescription": "Enables the open command without any pre-configured scope." + }, + { + "description": "Enables the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-save", + "markdownDescription": "Enables the save command without any pre-configured scope." + }, + { + "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)", + "type": "string", + "const": "dialog:deny-ask", + "markdownDescription": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" + }, + { + "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)", + "type": "string", + "const": "dialog:deny-confirm", + "markdownDescription": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" + }, + { + "description": "Denies the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-message", + "markdownDescription": "Denies the message command without any pre-configured scope." + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-open", + "markdownDescription": "Denies the open command without any pre-configured scope." + }, + { + "description": "Denies the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-save", + "markdownDescription": "Denies the save command without any pre-configured scope." + } + ] + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + } + } +} \ No newline at end of file diff --git a/frontend-ng/data-engine/src-tauri/gen/schemas/windows-schema.json b/frontend-ng/data-engine/src-tauri/gen/schemas/windows-schema.json new file mode 100644 index 0000000..a1f4665 --- /dev/null +++ b/frontend-ng/data-engine/src-tauri/gen/schemas/windows-schema.json @@ -0,0 +1,2310 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CapabilityFile", + "description": "Capability formats accepted in a capability file.", + "anyOf": [ + { + "description": "A single capability.", + "allOf": [ + { + "$ref": "#/definitions/Capability" + } + ] + }, + { + "description": "A list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + }, + { + "description": "A list of capabilities.", + "type": "object", + "required": [ + "capabilities" + ], + "properties": { + "capabilities": { + "description": "The list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + } + } + } + ], + "definitions": { + "Capability": { + "description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```", + "type": "object", + "required": [ + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "Identifier of the capability.\n\n## Example\n\n`main-user-files-write`", + "type": "string" + }, + "description": { + "description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.", + "default": "", + "type": "string" + }, + "remote": { + "description": "Configure remote URLs that can use the capability permissions.\n\nThis setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.\n\n:::caution Make sure you understand the security implications of providing remote sources with local system access. :::\n\n## Example\n\n```json { \"urls\": [\"https://*.mydomain.dev\"] } ```", + "anyOf": [ + { + "$ref": "#/definitions/CapabilityRemote" + }, + { + "type": "null" + } + ] + }, + "local": { + "description": "Whether this capability is enabled for local app URLs or not. Defaults to `true`.", + "default": true, + "type": "boolean" + }, + "windows": { + "description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nIf a window label matches any of the patterns in this list, the capability will be enabled on all the webviews of that window, regardless of the value of [`Self::webviews`].\n\nOn multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "webviews": { + "description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThe capability will be enabled on all the webviews whose label matches any of the patterns in this list, regardless of whether the webview's window label matches a pattern in [`Self::windows`].\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "permissions": { + "description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ] ```", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionEntry" + }, + "uniqueItems": true + }, + "platforms": { + "description": "Limit which target platforms this capability applies to.\n\nBy default all platforms are targeted.\n\n## Example\n\n`[\"macOS\",\"windows\"]`", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "CapabilityRemote": { + "description": "Configuration for remote URLs that are associated with the capability.", + "type": "object", + "required": [ + "urls" + ], + "properties": { + "urls": { + "description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n## Examples\n\n- \"https://*.mydomain.dev\": allows subdomains of mydomain.dev - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionEntry": { + "description": "An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`] or an object that references a permission and extends its scope.", + "anyOf": [ + { + "description": "Reference a permission or permission set by identifier.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + { + "description": "Reference a permission or permission set by identifier and extends its scope.", + "type": "object", + "allOf": [ + { + "properties": { + "identifier": { + "description": "Identifier of the permission or permission set.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + } + ], + "required": [ + "identifier" + ] + } + ] + }, + "Identifier": { + "description": "Permission identifier", + "oneOf": [ + { + "description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`", + "type": "string", + "const": "core:default", + "markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`" + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`", + "type": "string", + "const": "core:app:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`" + }, + { + "description": "Enables the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-hide", + "markdownDescription": "Enables the app_hide command without any pre-configured scope." + }, + { + "description": "Enables the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-show", + "markdownDescription": "Enables the app_show command without any pre-configured scope." + }, + { + "description": "Enables the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-bundle-type", + "markdownDescription": "Enables the bundle_type command without any pre-configured scope." + }, + { + "description": "Enables the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-default-window-icon", + "markdownDescription": "Enables the default_window_icon command without any pre-configured scope." + }, + { + "description": "Enables the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-fetch-data-store-identifiers", + "markdownDescription": "Enables the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Enables the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-identifier", + "markdownDescription": "Enables the identifier command without any pre-configured scope." + }, + { + "description": "Enables the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-name", + "markdownDescription": "Enables the name command without any pre-configured scope." + }, + { + "description": "Enables the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-register-listener", + "markdownDescription": "Enables the register_listener command without any pre-configured scope." + }, + { + "description": "Enables the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-data-store", + "markdownDescription": "Enables the remove_data_store command without any pre-configured scope." + }, + { + "description": "Enables the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-listener", + "markdownDescription": "Enables the remove_listener command without any pre-configured scope." + }, + { + "description": "Enables the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-app-theme", + "markdownDescription": "Enables the set_app_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-dock-visibility", + "markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Enables the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-tauri-version", + "markdownDescription": "Enables the tauri_version command without any pre-configured scope." + }, + { + "description": "Enables the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-version", + "markdownDescription": "Enables the version command without any pre-configured scope." + }, + { + "description": "Denies the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-hide", + "markdownDescription": "Denies the app_hide command without any pre-configured scope." + }, + { + "description": "Denies the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-show", + "markdownDescription": "Denies the app_show command without any pre-configured scope." + }, + { + "description": "Denies the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-bundle-type", + "markdownDescription": "Denies the bundle_type command without any pre-configured scope." + }, + { + "description": "Denies the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-default-window-icon", + "markdownDescription": "Denies the default_window_icon command without any pre-configured scope." + }, + { + "description": "Denies the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-fetch-data-store-identifiers", + "markdownDescription": "Denies the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Denies the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-identifier", + "markdownDescription": "Denies the identifier command without any pre-configured scope." + }, + { + "description": "Denies the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-name", + "markdownDescription": "Denies the name command without any pre-configured scope." + }, + { + "description": "Denies the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-register-listener", + "markdownDescription": "Denies the register_listener command without any pre-configured scope." + }, + { + "description": "Denies the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-data-store", + "markdownDescription": "Denies the remove_data_store command without any pre-configured scope." + }, + { + "description": "Denies the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-listener", + "markdownDescription": "Denies the remove_listener command without any pre-configured scope." + }, + { + "description": "Denies the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-app-theme", + "markdownDescription": "Denies the set_app_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-dock-visibility", + "markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Denies the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-tauri-version", + "markdownDescription": "Denies the tauri_version command without any pre-configured scope." + }, + { + "description": "Denies the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-version", + "markdownDescription": "Denies the version command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`", + "type": "string", + "const": "core:event:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`" + }, + { + "description": "Enables the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit", + "markdownDescription": "Enables the emit command without any pre-configured scope." + }, + { + "description": "Enables the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit-to", + "markdownDescription": "Enables the emit_to command without any pre-configured scope." + }, + { + "description": "Enables the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-listen", + "markdownDescription": "Enables the listen command without any pre-configured scope." + }, + { + "description": "Enables the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-unlisten", + "markdownDescription": "Enables the unlisten command without any pre-configured scope." + }, + { + "description": "Denies the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit", + "markdownDescription": "Denies the emit command without any pre-configured scope." + }, + { + "description": "Denies the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit-to", + "markdownDescription": "Denies the emit_to command without any pre-configured scope." + }, + { + "description": "Denies the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-listen", + "markdownDescription": "Denies the listen command without any pre-configured scope." + }, + { + "description": "Denies the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-unlisten", + "markdownDescription": "Denies the unlisten command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`", + "type": "string", + "const": "core:image:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`" + }, + { + "description": "Enables the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-bytes", + "markdownDescription": "Enables the from_bytes command without any pre-configured scope." + }, + { + "description": "Enables the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-path", + "markdownDescription": "Enables the from_path command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-rgba", + "markdownDescription": "Enables the rgba command without any pre-configured scope." + }, + { + "description": "Enables the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-size", + "markdownDescription": "Enables the size command without any pre-configured scope." + }, + { + "description": "Denies the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-bytes", + "markdownDescription": "Denies the from_bytes command without any pre-configured scope." + }, + { + "description": "Denies the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-path", + "markdownDescription": "Denies the from_path command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-rgba", + "markdownDescription": "Denies the rgba command without any pre-configured scope." + }, + { + "description": "Denies the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-size", + "markdownDescription": "Denies the size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`", + "type": "string", + "const": "core:menu:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`" + }, + { + "description": "Enables the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-append", + "markdownDescription": "Enables the append command without any pre-configured scope." + }, + { + "description": "Enables the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-create-default", + "markdownDescription": "Enables the create_default command without any pre-configured scope." + }, + { + "description": "Enables the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-get", + "markdownDescription": "Enables the get command without any pre-configured scope." + }, + { + "description": "Enables the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-insert", + "markdownDescription": "Enables the insert command without any pre-configured scope." + }, + { + "description": "Enables the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-checked", + "markdownDescription": "Enables the is_checked command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-items", + "markdownDescription": "Enables the items command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-popup", + "markdownDescription": "Enables the popup command without any pre-configured scope." + }, + { + "description": "Enables the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-prepend", + "markdownDescription": "Enables the prepend command without any pre-configured scope." + }, + { + "description": "Enables the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove", + "markdownDescription": "Enables the remove command without any pre-configured scope." + }, + { + "description": "Enables the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove-at", + "markdownDescription": "Enables the remove_at command without any pre-configured scope." + }, + { + "description": "Enables the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-accelerator", + "markdownDescription": "Enables the set_accelerator command without any pre-configured scope." + }, + { + "description": "Enables the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-app-menu", + "markdownDescription": "Enables the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-help-menu-for-nsapp", + "markdownDescription": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-window-menu", + "markdownDescription": "Enables the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-windows-menu-for-nsapp", + "markdownDescription": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-checked", + "markdownDescription": "Enables the set_checked command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-text", + "markdownDescription": "Enables the set_text command without any pre-configured scope." + }, + { + "description": "Enables the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-text", + "markdownDescription": "Enables the text command without any pre-configured scope." + }, + { + "description": "Denies the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-append", + "markdownDescription": "Denies the append command without any pre-configured scope." + }, + { + "description": "Denies the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-create-default", + "markdownDescription": "Denies the create_default command without any pre-configured scope." + }, + { + "description": "Denies the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-get", + "markdownDescription": "Denies the get command without any pre-configured scope." + }, + { + "description": "Denies the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-insert", + "markdownDescription": "Denies the insert command without any pre-configured scope." + }, + { + "description": "Denies the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-checked", + "markdownDescription": "Denies the is_checked command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-items", + "markdownDescription": "Denies the items command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-popup", + "markdownDescription": "Denies the popup command without any pre-configured scope." + }, + { + "description": "Denies the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-prepend", + "markdownDescription": "Denies the prepend command without any pre-configured scope." + }, + { + "description": "Denies the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove", + "markdownDescription": "Denies the remove command without any pre-configured scope." + }, + { + "description": "Denies the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove-at", + "markdownDescription": "Denies the remove_at command without any pre-configured scope." + }, + { + "description": "Denies the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-accelerator", + "markdownDescription": "Denies the set_accelerator command without any pre-configured scope." + }, + { + "description": "Denies the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-app-menu", + "markdownDescription": "Denies the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-help-menu-for-nsapp", + "markdownDescription": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-window-menu", + "markdownDescription": "Denies the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-windows-menu-for-nsapp", + "markdownDescription": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-checked", + "markdownDescription": "Denies the set_checked command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-text", + "markdownDescription": "Denies the set_text command without any pre-configured scope." + }, + { + "description": "Denies the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-text", + "markdownDescription": "Denies the text command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`", + "type": "string", + "const": "core:path:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`" + }, + { + "description": "Enables the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-basename", + "markdownDescription": "Enables the basename command without any pre-configured scope." + }, + { + "description": "Enables the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-dirname", + "markdownDescription": "Enables the dirname command without any pre-configured scope." + }, + { + "description": "Enables the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-extname", + "markdownDescription": "Enables the extname command without any pre-configured scope." + }, + { + "description": "Enables the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-is-absolute", + "markdownDescription": "Enables the is_absolute command without any pre-configured scope." + }, + { + "description": "Enables the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-join", + "markdownDescription": "Enables the join command without any pre-configured scope." + }, + { + "description": "Enables the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-normalize", + "markdownDescription": "Enables the normalize command without any pre-configured scope." + }, + { + "description": "Enables the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve", + "markdownDescription": "Enables the resolve command without any pre-configured scope." + }, + { + "description": "Enables the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve-directory", + "markdownDescription": "Enables the resolve_directory command without any pre-configured scope." + }, + { + "description": "Denies the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-basename", + "markdownDescription": "Denies the basename command without any pre-configured scope." + }, + { + "description": "Denies the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-dirname", + "markdownDescription": "Denies the dirname command without any pre-configured scope." + }, + { + "description": "Denies the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-extname", + "markdownDescription": "Denies the extname command without any pre-configured scope." + }, + { + "description": "Denies the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-is-absolute", + "markdownDescription": "Denies the is_absolute command without any pre-configured scope." + }, + { + "description": "Denies the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-join", + "markdownDescription": "Denies the join command without any pre-configured scope." + }, + { + "description": "Denies the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-normalize", + "markdownDescription": "Denies the normalize command without any pre-configured scope." + }, + { + "description": "Denies the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve", + "markdownDescription": "Denies the resolve command without any pre-configured scope." + }, + { + "description": "Denies the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve-directory", + "markdownDescription": "Denies the resolve_directory command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`", + "type": "string", + "const": "core:resources:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`" + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`", + "type": "string", + "const": "core:tray:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`" + }, + { + "description": "Enables the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-get-by-id", + "markdownDescription": "Enables the get_by_id command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-remove-by-id", + "markdownDescription": "Enables the remove_by_id command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon-as-template", + "markdownDescription": "Enables the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Enables the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-menu", + "markdownDescription": "Enables the set_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-show-menu-on-left-click", + "markdownDescription": "Enables the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Enables the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-temp-dir-path", + "markdownDescription": "Enables the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-tooltip", + "markdownDescription": "Enables the set_tooltip command without any pre-configured scope." + }, + { + "description": "Enables the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-visible", + "markdownDescription": "Enables the set_visible command without any pre-configured scope." + }, + { + "description": "Denies the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-get-by-id", + "markdownDescription": "Denies the get_by_id command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-remove-by-id", + "markdownDescription": "Denies the remove_by_id command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon-as-template", + "markdownDescription": "Denies the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Denies the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-menu", + "markdownDescription": "Denies the set_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-show-menu-on-left-click", + "markdownDescription": "Denies the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Denies the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-temp-dir-path", + "markdownDescription": "Denies the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-tooltip", + "markdownDescription": "Denies the set_tooltip command without any pre-configured scope." + }, + { + "description": "Denies the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-visible", + "markdownDescription": "Denies the set_visible command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`", + "type": "string", + "const": "core:webview:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`" + }, + { + "description": "Enables the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-clear-all-browsing-data", + "markdownDescription": "Enables the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Enables the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview", + "markdownDescription": "Enables the create_webview command without any pre-configured scope." + }, + { + "description": "Enables the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview-window", + "markdownDescription": "Enables the create_webview_window command without any pre-configured scope." + }, + { + "description": "Enables the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-get-all-webviews", + "markdownDescription": "Enables the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-internal-toggle-devtools", + "markdownDescription": "Enables the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Enables the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-print", + "markdownDescription": "Enables the print command without any pre-configured scope." + }, + { + "description": "Enables the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-reparent", + "markdownDescription": "Enables the reparent command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-auto-resize", + "markdownDescription": "Enables the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-background-color", + "markdownDescription": "Enables the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-focus", + "markdownDescription": "Enables the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-position", + "markdownDescription": "Enables the set_webview_position command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-size", + "markdownDescription": "Enables the set_webview_size command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-zoom", + "markdownDescription": "Enables the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Enables the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-close", + "markdownDescription": "Enables the webview_close command without any pre-configured scope." + }, + { + "description": "Enables the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-hide", + "markdownDescription": "Enables the webview_hide command without any pre-configured scope." + }, + { + "description": "Enables the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-position", + "markdownDescription": "Enables the webview_position command without any pre-configured scope." + }, + { + "description": "Enables the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-show", + "markdownDescription": "Enables the webview_show command without any pre-configured scope." + }, + { + "description": "Enables the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-size", + "markdownDescription": "Enables the webview_size command without any pre-configured scope." + }, + { + "description": "Denies the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-clear-all-browsing-data", + "markdownDescription": "Denies the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Denies the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview", + "markdownDescription": "Denies the create_webview command without any pre-configured scope." + }, + { + "description": "Denies the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview-window", + "markdownDescription": "Denies the create_webview_window command without any pre-configured scope." + }, + { + "description": "Denies the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-get-all-webviews", + "markdownDescription": "Denies the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-internal-toggle-devtools", + "markdownDescription": "Denies the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Denies the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-print", + "markdownDescription": "Denies the print command without any pre-configured scope." + }, + { + "description": "Denies the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-reparent", + "markdownDescription": "Denies the reparent command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-auto-resize", + "markdownDescription": "Denies the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-background-color", + "markdownDescription": "Denies the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-focus", + "markdownDescription": "Denies the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-position", + "markdownDescription": "Denies the set_webview_position command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-size", + "markdownDescription": "Denies the set_webview_size command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-zoom", + "markdownDescription": "Denies the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Denies the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-close", + "markdownDescription": "Denies the webview_close command without any pre-configured scope." + }, + { + "description": "Denies the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-hide", + "markdownDescription": "Denies the webview_hide command without any pre-configured scope." + }, + { + "description": "Denies the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-position", + "markdownDescription": "Denies the webview_position command without any pre-configured scope." + }, + { + "description": "Denies the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-show", + "markdownDescription": "Denies the webview_show command without any pre-configured scope." + }, + { + "description": "Denies the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-size", + "markdownDescription": "Denies the webview_size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`", + "type": "string", + "const": "core:window:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`" + }, + { + "description": "Enables the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-available-monitors", + "markdownDescription": "Enables the available_monitors command without any pre-configured scope." + }, + { + "description": "Enables the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-center", + "markdownDescription": "Enables the center command without any pre-configured scope." + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Enables the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-create", + "markdownDescription": "Enables the create command without any pre-configured scope." + }, + { + "description": "Enables the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-current-monitor", + "markdownDescription": "Enables the current_monitor command without any pre-configured scope." + }, + { + "description": "Enables the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-cursor-position", + "markdownDescription": "Enables the cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-destroy", + "markdownDescription": "Enables the destroy command without any pre-configured scope." + }, + { + "description": "Enables the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-get-all-windows", + "markdownDescription": "Enables the get_all_windows command without any pre-configured scope." + }, + { + "description": "Enables the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-hide", + "markdownDescription": "Enables the hide command without any pre-configured scope." + }, + { + "description": "Enables the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-position", + "markdownDescription": "Enables the inner_position command without any pre-configured scope." + }, + { + "description": "Enables the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-size", + "markdownDescription": "Enables the inner_size command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-internal-toggle-maximize", + "markdownDescription": "Enables the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-always-on-top", + "markdownDescription": "Enables the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-closable", + "markdownDescription": "Enables the is_closable command without any pre-configured scope." + }, + { + "description": "Enables the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-decorated", + "markdownDescription": "Enables the is_decorated command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-focused", + "markdownDescription": "Enables the is_focused command without any pre-configured scope." + }, + { + "description": "Enables the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-fullscreen", + "markdownDescription": "Enables the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximizable", + "markdownDescription": "Enables the is_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximized", + "markdownDescription": "Enables the is_maximized command without any pre-configured scope." + }, + { + "description": "Enables the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimizable", + "markdownDescription": "Enables the is_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimized", + "markdownDescription": "Enables the is_minimized command without any pre-configured scope." + }, + { + "description": "Enables the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-resizable", + "markdownDescription": "Enables the is_resizable command without any pre-configured scope." + }, + { + "description": "Enables the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-visible", + "markdownDescription": "Enables the is_visible command without any pre-configured scope." + }, + { + "description": "Enables the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-maximize", + "markdownDescription": "Enables the maximize command without any pre-configured scope." + }, + { + "description": "Enables the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-minimize", + "markdownDescription": "Enables the minimize command without any pre-configured scope." + }, + { + "description": "Enables the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-monitor-from-point", + "markdownDescription": "Enables the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Enables the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-position", + "markdownDescription": "Enables the outer_position command without any pre-configured scope." + }, + { + "description": "Enables the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-size", + "markdownDescription": "Enables the outer_size command without any pre-configured scope." + }, + { + "description": "Enables the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-primary-monitor", + "markdownDescription": "Enables the primary_monitor command without any pre-configured scope." + }, + { + "description": "Enables the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-request-user-attention", + "markdownDescription": "Enables the request_user_attention command without any pre-configured scope." + }, + { + "description": "Enables the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-scale-factor", + "markdownDescription": "Enables the scale_factor command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-bottom", + "markdownDescription": "Enables the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-top", + "markdownDescription": "Enables the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-background-color", + "markdownDescription": "Enables the set_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-count", + "markdownDescription": "Enables the set_badge_count command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-label", + "markdownDescription": "Enables the set_badge_label command without any pre-configured scope." + }, + { + "description": "Enables the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-closable", + "markdownDescription": "Enables the set_closable command without any pre-configured scope." + }, + { + "description": "Enables the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-content-protected", + "markdownDescription": "Enables the set_content_protected command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-grab", + "markdownDescription": "Enables the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-icon", + "markdownDescription": "Enables the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-position", + "markdownDescription": "Enables the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-visible", + "markdownDescription": "Enables the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Enables the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-decorations", + "markdownDescription": "Enables the set_decorations command without any pre-configured scope." + }, + { + "description": "Enables the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-effects", + "markdownDescription": "Enables the set_effects command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focus", + "markdownDescription": "Enables the set_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focusable", + "markdownDescription": "Enables the set_focusable command without any pre-configured scope." + }, + { + "description": "Enables the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-fullscreen", + "markdownDescription": "Enables the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-ignore-cursor-events", + "markdownDescription": "Enables the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Enables the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-max-size", + "markdownDescription": "Enables the set_max_size command without any pre-configured scope." + }, + { + "description": "Enables the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-maximizable", + "markdownDescription": "Enables the set_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-min-size", + "markdownDescription": "Enables the set_min_size command without any pre-configured scope." + }, + { + "description": "Enables the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-minimizable", + "markdownDescription": "Enables the set_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-overlay-icon", + "markdownDescription": "Enables the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-position", + "markdownDescription": "Enables the set_position command without any pre-configured scope." + }, + { + "description": "Enables the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-progress-bar", + "markdownDescription": "Enables the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Enables the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-resizable", + "markdownDescription": "Enables the set_resizable command without any pre-configured scope." + }, + { + "description": "Enables the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-shadow", + "markdownDescription": "Enables the set_shadow command without any pre-configured scope." + }, + { + "description": "Enables the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-simple-fullscreen", + "markdownDescription": "Enables the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size", + "markdownDescription": "Enables the set_size command without any pre-configured scope." + }, + { + "description": "Enables the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size-constraints", + "markdownDescription": "Enables the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Enables the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-skip-taskbar", + "markdownDescription": "Enables the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Enables the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-theme", + "markdownDescription": "Enables the set_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title-bar-style", + "markdownDescription": "Enables the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Enables the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-visible-on-all-workspaces", + "markdownDescription": "Enables the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Enables the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-show", + "markdownDescription": "Enables the show command without any pre-configured scope." + }, + { + "description": "Enables the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-dragging", + "markdownDescription": "Enables the start_dragging command without any pre-configured scope." + }, + { + "description": "Enables the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-resize-dragging", + "markdownDescription": "Enables the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Enables the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-theme", + "markdownDescription": "Enables the theme command without any pre-configured scope." + }, + { + "description": "Enables the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-title", + "markdownDescription": "Enables the title command without any pre-configured scope." + }, + { + "description": "Enables the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-toggle-maximize", + "markdownDescription": "Enables the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unmaximize", + "markdownDescription": "Enables the unmaximize command without any pre-configured scope." + }, + { + "description": "Enables the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unminimize", + "markdownDescription": "Enables the unminimize command without any pre-configured scope." + }, + { + "description": "Denies the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-available-monitors", + "markdownDescription": "Denies the available_monitors command without any pre-configured scope." + }, + { + "description": "Denies the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-center", + "markdownDescription": "Denies the center command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Denies the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-create", + "markdownDescription": "Denies the create command without any pre-configured scope." + }, + { + "description": "Denies the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-current-monitor", + "markdownDescription": "Denies the current_monitor command without any pre-configured scope." + }, + { + "description": "Denies the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-cursor-position", + "markdownDescription": "Denies the cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-destroy", + "markdownDescription": "Denies the destroy command without any pre-configured scope." + }, + { + "description": "Denies the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-get-all-windows", + "markdownDescription": "Denies the get_all_windows command without any pre-configured scope." + }, + { + "description": "Denies the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-hide", + "markdownDescription": "Denies the hide command without any pre-configured scope." + }, + { + "description": "Denies the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-position", + "markdownDescription": "Denies the inner_position command without any pre-configured scope." + }, + { + "description": "Denies the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-size", + "markdownDescription": "Denies the inner_size command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-internal-toggle-maximize", + "markdownDescription": "Denies the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-always-on-top", + "markdownDescription": "Denies the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-closable", + "markdownDescription": "Denies the is_closable command without any pre-configured scope." + }, + { + "description": "Denies the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-decorated", + "markdownDescription": "Denies the is_decorated command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-focused", + "markdownDescription": "Denies the is_focused command without any pre-configured scope." + }, + { + "description": "Denies the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-fullscreen", + "markdownDescription": "Denies the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximizable", + "markdownDescription": "Denies the is_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximized", + "markdownDescription": "Denies the is_maximized command without any pre-configured scope." + }, + { + "description": "Denies the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimizable", + "markdownDescription": "Denies the is_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimized", + "markdownDescription": "Denies the is_minimized command without any pre-configured scope." + }, + { + "description": "Denies the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-resizable", + "markdownDescription": "Denies the is_resizable command without any pre-configured scope." + }, + { + "description": "Denies the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-visible", + "markdownDescription": "Denies the is_visible command without any pre-configured scope." + }, + { + "description": "Denies the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-maximize", + "markdownDescription": "Denies the maximize command without any pre-configured scope." + }, + { + "description": "Denies the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-minimize", + "markdownDescription": "Denies the minimize command without any pre-configured scope." + }, + { + "description": "Denies the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-monitor-from-point", + "markdownDescription": "Denies the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Denies the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-position", + "markdownDescription": "Denies the outer_position command without any pre-configured scope." + }, + { + "description": "Denies the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-size", + "markdownDescription": "Denies the outer_size command without any pre-configured scope." + }, + { + "description": "Denies the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-primary-monitor", + "markdownDescription": "Denies the primary_monitor command without any pre-configured scope." + }, + { + "description": "Denies the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-request-user-attention", + "markdownDescription": "Denies the request_user_attention command without any pre-configured scope." + }, + { + "description": "Denies the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-scale-factor", + "markdownDescription": "Denies the scale_factor command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-bottom", + "markdownDescription": "Denies the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-top", + "markdownDescription": "Denies the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-background-color", + "markdownDescription": "Denies the set_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-count", + "markdownDescription": "Denies the set_badge_count command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-label", + "markdownDescription": "Denies the set_badge_label command without any pre-configured scope." + }, + { + "description": "Denies the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-closable", + "markdownDescription": "Denies the set_closable command without any pre-configured scope." + }, + { + "description": "Denies the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-content-protected", + "markdownDescription": "Denies the set_content_protected command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-grab", + "markdownDescription": "Denies the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-icon", + "markdownDescription": "Denies the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-position", + "markdownDescription": "Denies the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-visible", + "markdownDescription": "Denies the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Denies the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-decorations", + "markdownDescription": "Denies the set_decorations command without any pre-configured scope." + }, + { + "description": "Denies the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-effects", + "markdownDescription": "Denies the set_effects command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focus", + "markdownDescription": "Denies the set_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focusable", + "markdownDescription": "Denies the set_focusable command without any pre-configured scope." + }, + { + "description": "Denies the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-fullscreen", + "markdownDescription": "Denies the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-ignore-cursor-events", + "markdownDescription": "Denies the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Denies the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-max-size", + "markdownDescription": "Denies the set_max_size command without any pre-configured scope." + }, + { + "description": "Denies the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-maximizable", + "markdownDescription": "Denies the set_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-min-size", + "markdownDescription": "Denies the set_min_size command without any pre-configured scope." + }, + { + "description": "Denies the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-minimizable", + "markdownDescription": "Denies the set_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-overlay-icon", + "markdownDescription": "Denies the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-position", + "markdownDescription": "Denies the set_position command without any pre-configured scope." + }, + { + "description": "Denies the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-progress-bar", + "markdownDescription": "Denies the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Denies the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-resizable", + "markdownDescription": "Denies the set_resizable command without any pre-configured scope." + }, + { + "description": "Denies the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-shadow", + "markdownDescription": "Denies the set_shadow command without any pre-configured scope." + }, + { + "description": "Denies the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-simple-fullscreen", + "markdownDescription": "Denies the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size", + "markdownDescription": "Denies the set_size command without any pre-configured scope." + }, + { + "description": "Denies the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size-constraints", + "markdownDescription": "Denies the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Denies the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-skip-taskbar", + "markdownDescription": "Denies the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Denies the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-theme", + "markdownDescription": "Denies the set_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title-bar-style", + "markdownDescription": "Denies the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Denies the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-visible-on-all-workspaces", + "markdownDescription": "Denies the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Denies the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-show", + "markdownDescription": "Denies the show command without any pre-configured scope." + }, + { + "description": "Denies the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-dragging", + "markdownDescription": "Denies the start_dragging command without any pre-configured scope." + }, + { + "description": "Denies the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-resize-dragging", + "markdownDescription": "Denies the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Denies the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-theme", + "markdownDescription": "Denies the theme command without any pre-configured scope." + }, + { + "description": "Denies the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-title", + "markdownDescription": "Denies the title command without any pre-configured scope." + }, + { + "description": "Denies the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-toggle-maximize", + "markdownDescription": "Denies the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unmaximize", + "markdownDescription": "Denies the unmaximize command without any pre-configured scope." + }, + { + "description": "Denies the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unminimize", + "markdownDescription": "Denies the unminimize command without any pre-configured scope." + }, + { + "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\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`", + "type": "string", + "const": "dialog:default", + "markdownDescription": "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\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`" + }, + { + "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)", + "type": "string", + "const": "dialog:allow-ask", + "markdownDescription": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" + }, + { + "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)", + "type": "string", + "const": "dialog:allow-confirm", + "markdownDescription": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" + }, + { + "description": "Enables the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-message", + "markdownDescription": "Enables the message command without any pre-configured scope." + }, + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-open", + "markdownDescription": "Enables the open command without any pre-configured scope." + }, + { + "description": "Enables the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-save", + "markdownDescription": "Enables the save command without any pre-configured scope." + }, + { + "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)", + "type": "string", + "const": "dialog:deny-ask", + "markdownDescription": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" + }, + { + "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)", + "type": "string", + "const": "dialog:deny-confirm", + "markdownDescription": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" + }, + { + "description": "Denies the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-message", + "markdownDescription": "Denies the message command without any pre-configured scope." + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-open", + "markdownDescription": "Denies the open command without any pre-configured scope." + }, + { + "description": "Denies the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-save", + "markdownDescription": "Denies the save command without any pre-configured scope." + } + ] + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + } + } +} \ No newline at end of file diff --git a/frontend-ng/data-engine/src-tauri/icons/icon.ico b/frontend-ng/data-engine/src-tauri/icons/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..57614f9c967596fad0a3989bec2b1deff33034f6 GIT binary patch literal 15086 zcmd^G33O9Omi+`8$@{|M-I6TH3wzF-p5CV8o}7f~KxR60LK+ApEFB<$bcciv%@SmA zV{n>g85YMFFeU*Uvl=i4v)C*qgnb;$GQ=3XTe9{Y%c`mO%su)noNCCQ*@t1WXn|B(hQ7i~ zrUK8|pUkD6#lNo!bt$6)jR!&C?`P5G(`e((P($RaLeq+o0Vd~f11;qB05kdbAOm?r zXv~GYr_sibQO9NGTCdT;+G(!{4Xs@4fPak8#L8PjgJwcs-Mm#nR_Z0s&u?nDX5^~@ z+A6?}g0|=4e_LoE69pPFO`yCD@BCjgKpzMH0O4Xs{Ahc?K3HC5;l=f zg>}alhBXX&);z$E-wai+9TTRtBX-bWYY@cl$@YN#gMd~tM_5lj6W%8ah4;uZ;jP@Q zVbuel1rPA?2@x9Y+u?e`l{Z4ngfG5q5BLH5QsEu4GVpt{KIp1?U)=3+KQ;%7ec8l* zdV=zZgN5>O3G(3L2fqj3;oBbZZw$Ij@`Juz@?+yy#OPw)>#wsTewVgTK9BGt5AbZ&?K&B3GVF&yu?@(Xj3fR3n+ZP0%+wo)D9_xp>Z$`A4 zfV>}NWjO#3lqumR0`gvnffd9Ka}JJMuHS&|55-*mCD#8e^anA<+sFZVaJe7{=p*oX zE_Uv?1>e~ga=seYzh{9P+n5<+7&9}&(kwqSaz;1aD|YM3HBiy<))4~QJSIryyqp| z8nGc(8>3(_nEI4n)n7j(&d4idW1tVLjZ7QbNLXg;LB ziHsS5pXHEjGJZb59KcvS~wv;uZR-+4qEqow`;JCfB*+b^UL^3!?;-^F%yt=VjU|v z39SSqKcRu_NVvz!zJzL0CceJaS6%!(eMshPv_0U5G`~!a#I$qI5Ic(>IONej@aH=f z)($TAT#1I{iCS4f{D2+ApS=$3E7}5=+y(rA9mM#;Cky%b*Gi0KfFA`ofKTzu`AV-9 znW|y@19rrZ*!N2AvDi<_ZeR3O2R{#dh1#3-d%$k${Rx42h+i&GZo5!C^dSL34*AKp z27mTd>k>?V&X;Nl%GZ(>0s`1UN~Hfyj>KPjtnc|)xM@{H_B9rNr~LuH`Gr5_am&Ep zTjZA8hljNj5H1Ipm-uD9rC}U{-vR!eay5&6x6FkfupdpT*84MVwGpdd(}ib)zZ3Ky z7C$pnjc82(W_y_F{PhYj?o!@3__UUvpX)v69aBSzYj3 zdi}YQkKs^SyXyFG2LTRz9{(w}y~!`{EuAaUr6G1M{*%c+kP1olW9z23dSH!G4_HSK zzae-DF$OGR{ofP*!$a(r^5Go>I3SObVI6FLY)N@o<*gl0&kLo-OT{Tl*7nCz>Iq=? zcigIDHtj|H;6sR?or8Wd_a4996GI*CXGU}o;D9`^FM!AT1pBY~?|4h^61BY#_yIfO zKO?E0 zJ{Pc`9rVEI&$xxXu`<5E)&+m(7zX^v0rqofLs&bnQT(1baQkAr^kEsk)15vlzAZ-l z@OO9RF<+IiJ*O@HE256gCt!bF=NM*vh|WVWmjVawcNoksRTMvR03H{p@cjwKh(CL4 z7_PB(dM=kO)!s4fW!1p0f93YN@?ZSG` z$B!JaAJCtW$B97}HNO9(x-t30&E}Mo1UPi@Av%uHj~?T|!4JLwV;KCx8xO#b9IlUW zI6+{a@Wj|<2Y=U;a@vXbxqZNngH8^}LleE_4*0&O7#3iGxfJ%Id>+sb;7{L=aIic8 z|EW|{{S)J-wr@;3PmlxRXU8!e2gm_%s|ReH!reFcY8%$Hl4M5>;6^UDUUae?kOy#h zk~6Ee_@ZAn48Bab__^bNmQ~+k=02jz)e0d9Z3>G?RGG!65?d1>9}7iG17?P*=GUV-#SbLRw)Hu{zx*azHxWkGNTWl@HeWjA?39Ia|sCi{e;!^`1Oec zb>Z|b65OM*;eC=ZLSy?_fg$&^2xI>qSLA2G*$nA3GEnp3$N-)46`|36m*sc#4%C|h zBN<2U;7k>&G_wL4=Ve5z`ubVD&*Hxi)r@{4RCDw7U_D`lbC(9&pG5C*z#W>8>HU)h z!h3g?2UL&sS!oY5$3?VlA0Me9W5e~V;2jds*fz^updz#AJ%G8w2V}AEE?E^=MK%Xt z__Bx1cr7+DQmuHmzn*|hh%~eEc9@m05@clWfpEFcr+06%0&dZJH&@8^&@*$qR@}o3 z@Tuuh2FsLz^zH+dN&T&?0G3I?MpmYJ;GP$J!EzjeM#YLJ!W$}MVNb0^HfOA>5Fe~UNn%Zk(PT@~9}1dt)1UQ zU*B5K?Dl#G74qmg|2>^>0WtLX#Jz{lO4NT`NYB*(L#D|5IpXr9v&7a@YsGp3vLR7L zHYGHZg7{ie6n~2p$6Yz>=^cEg7tEgk-1YRl%-s7^cbqFb(U7&Dp78+&ut5!Tn(hER z|Gp4Ed@CnOPeAe|N>U(dB;SZ?NU^AzoD^UAH_vamp6Ws}{|mSq`^+VP1g~2B{%N-!mWz<`)G)>V-<`9`L4?3dM%Qh6<@kba+m`JS{Ya@9Fq*m6$$ zA1%Ogc~VRH33|S9l%CNb4zM%k^EIpqY}@h{w(aBcJ9c05oiZx#SK9t->5lSI`=&l~ z+-Ic)a{FbBhXV$Xt!WRd`R#Jk-$+_Z52rS>?Vpt2IK<84|E-SBEoIw>cs=a{BlQ7O z-?{Fy_M&84&9|KM5wt~)*!~i~E=(6m8(uCO)I=)M?)&sRbzH$9Rovzd?ZEY}GqX+~ zFbEbLz`BZ49=2Yh-|<`waK-_4!7`ro@zlC|r&I4fc4oyb+m=|c8)8%tZ-z5FwhzDt zL5kB@u53`d@%nHl0Sp)Dw`(QU&>vujEn?GPEXUW!Wi<+4e%BORl&BIH+SwRcbS}X@ z01Pk|vA%OdJKAs17zSXtO55k!;%m9>1eW9LnyAX4uj7@${O6cfii`49qTNItzny5J zH&Gj`e}o}?xjQ}r?LrI%FjUd@xflT3|7LA|ka%Q3i}a8gVm<`HIWoJGH=$EGClX^C0lysQJ>UO(q&;`T#8txuoQ_{l^kEV9CAdXuU1Ghg8 zN_6hHFuy&1x24q5-(Z7;!poYdt*`UTdrQOIQ!2O7_+AHV2hgXaEz7)>$LEdG z<8vE^Tw$|YwZHZDPM!SNOAWG$?J)MdmEk{U!!$M#fp7*Wo}jJ$Q(=8>R`Ats?e|VU?Zt7Cdh%AdnfyN3MBWw{ z$OnREvPf7%z6`#2##_7id|H%Y{vV^vWXb?5d5?a_y&t3@p9t$ncHj-NBdo&X{wrfJ zamN)VMYROYh_SvjJ=Xd!Ga?PY_$;*L=SxFte!4O6%0HEh%iZ4=gvns7IWIyJHa|hT z2;1+e)`TvbNb3-0z&DD_)Jomsg-7p_Uh`wjGnU1urmv1_oVqRg#=C?e?!7DgtqojU zWoAB($&53;TsXu^@2;8M`#z{=rPy?JqgYM0CDf4v@z=ZD|ItJ&8%_7A#K?S{wjxgd z?xA6JdJojrWpB7fr2p_MSsU4(R7=XGS0+Eg#xR=j>`H@R9{XjwBmqAiOxOL` zt?XK-iTEOWV}f>Pz3H-s*>W z4~8C&Xq25UQ^xH6H9kY_RM1$ch+%YLF72AA7^b{~VNTG}Tj#qZltz5Q=qxR`&oIlW Nr__JTFzvMr^FKp4S3v*( literal 0 HcmV?d00001 diff --git a/frontend-ng/data-engine/src-tauri/tauri.conf.json b/frontend-ng/data-engine/src-tauri/tauri.conf.json index 4981b27..b0d0e5c 100644 --- a/frontend-ng/data-engine/src-tauri/tauri.conf.json +++ b/frontend-ng/data-engine/src-tauri/tauri.conf.json @@ -24,6 +24,9 @@ }, "bundle": { "active": true, - "targets": "all" + "targets": "all", + "icon": [ + "icons/icon.ico" + ] } } diff --git a/frontend-ng/data-engine/src-tauri/tauri.npm.conf.json b/frontend-ng/data-engine/src-tauri/tauri.npm.conf.json new file mode 100644 index 0000000..3e01a8f --- /dev/null +++ b/frontend-ng/data-engine/src-tauri/tauri.npm.conf.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "Data Engine", + "version": "0.0.0", + "identifier": "com.rumarino.dataengine", + "build": { + "beforeDevCommand": "npm run start -- --host 127.0.0.1 --port 4200", + "devUrl": "http://127.0.0.1:4200", + "beforeBuildCommand": "npm run build", + "frontendDist": "../dist/data-engine/browser" + }, + "app": { + "windows": [ + { + "label": "main", + "title": "Data Engine", + "width": 1600, + "height": 1000, + "minWidth": 1200, + "minHeight": 720, + "resizable": true + } + ] + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/icon.ico" + ] + } +} From 5378901cf2a4d3b3ee618b76ef97b84139b05f83 Mon Sep 17 00:00:00 2001 From: "Rafael A." <157764758+HarenDev@users.noreply.github.com> Date: Mon, 27 Apr 2026 22:43:39 -0400 Subject: [PATCH 16/30] adding better transparency in the frontend for jobs --- backend/api.py | 535 +++++++++++++++++- backend/tests/tester.py | 87 ++- backend/utils.py | 14 +- frontend-ng/data-engine/angular.json | 4 +- frontend-ng/data-engine/package-lock.json | 280 ++++++++- .../src/app/services/backend.service.spec.ts | 24 + .../src/app/services/backend.service.ts | 84 ++- .../video-masker/video-masker.component.css | 195 ++++++- .../video-masker/video-masker.component.html | 35 +- .../video-masker.component.spec.ts | 97 ++++ .../video-masker/video-masker.component.ts | 221 ++++++-- 11 files changed, 1453 insertions(+), 123 deletions(-) diff --git a/backend/api.py b/backend/api.py index c5ed54e..41a8b83 100644 --- a/backend/api.py +++ b/backend/api.py @@ -1,4 +1,4 @@ -from typing import Optional, Any +from typing import Optional, Any, Callable from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel @@ -18,6 +18,7 @@ import cv2 import shutil import uuid +import threading app = FastAPI() @@ -52,6 +53,196 @@ DEFAULT_PROMPT_TRACK_BATCH_SIZE = int(os.getenv("TRACK_PROMPT_BATCH_SIZE", "32")) logger = logging.getLogger(__name__) +current_job: Optional[dict[str, Any]] = None +current_job_lock = threading.Lock() +_UNSET = object() + + +def _utc_now_iso() -> str: + return datetime.utcnow().isoformat(timespec="seconds") + "Z" + + +def _serialize_job(job: Optional[dict[str, Any]] = None) -> Optional[dict[str, Any]]: + with current_job_lock: + source = current_job if job is None else job + return dict(source) if source is not None else None + + +def _active_job_exists() -> bool: + return current_job is not None and current_job.get("status") in {"queued", "running"} + + +def _start_job(operation: str, *, stage: str, stage_label: str, message: str) -> dict[str, Any]: + global current_job + with current_job_lock: + if _active_job_exists(): + raise HTTPException(status_code=409, detail="Another operation is already running.") + + now = _utc_now_iso() + current_job = { + "job_id": uuid.uuid4().hex, + "operation": operation, + "status": "queued", + "stage": stage, + "stage_label": stage_label, + "progress": 0.0, + "current": None, + "total": None, + "window_index": None, + "window_count": None, + "frame_idx": None, + "stage_history": [], + "message": message, + "result": None, + "error": None, + "started_at": now, + "updated_at": now, + "completed_at": None, + } + return dict(current_job) + + +def _update_job( + *, + status: Optional[str] = None, + stage: Optional[str] = None, + stage_label: Optional[str] = None, + progress: Any = _UNSET, + current: Any = _UNSET, + total: Any = _UNSET, + window_index: Any = _UNSET, + window_count: Any = _UNSET, + frame_idx: Any = _UNSET, + message: Optional[str] = None, + append_history: bool = True, +) -> None: + with current_job_lock: + if current_job is None: + return + if status is not None: + current_job["status"] = status + if stage is not None: + current_job["stage"] = stage + if stage_label is not None: + current_job["stage_label"] = stage_label + if progress is not _UNSET: + current_job["progress"] = None if progress is None else min(max(float(progress), 0.0), 1.0) + if current is not _UNSET: + current_job["current"] = None if current is None else int(current) + if total is not _UNSET: + current_job["total"] = None if total is None else int(total) + if window_index is not _UNSET: + current_job["window_index"] = None if window_index is None else int(window_index) + if window_count is not _UNSET: + current_job["window_count"] = None if window_count is None else int(window_count) + if frame_idx is not _UNSET: + current_job["frame_idx"] = None if frame_idx is None else int(frame_idx) + if message is not None: + current_job["message"] = message + now = _utc_now_iso() + current_job["updated_at"] = now + if append_history and (stage is not None or stage_label is not None or message is not None): + history = current_job.setdefault("stage_history", []) + history.append( + { + "stage": current_job.get("stage"), + "stage_label": current_job.get("stage_label"), + "message": current_job.get("message"), + "progress": current_job.get("progress"), + "updated_at": now, + } + ) + del history[:-8] + + +def _complete_job(result: dict[str, Any]) -> None: + with current_job_lock: + if current_job is None: + return + now = _utc_now_iso() + current_job.update( + { + "status": "completed", + "stage": "completed", + "stage_label": "Completed", + "progress": 1.0, + "current": current_job.get("total") or current_job.get("current"), + "window_index": None, + "window_count": None, + "frame_idx": None, + "message": "Operation completed", + "result": result, + "error": None, + "updated_at": now, + "completed_at": now, + } + ) + + +def _fail_job(error_code: str, message: str, detail: Optional[str] = None) -> None: + with current_job_lock: + if current_job is None: + return + now = _utc_now_iso() + current_job.update( + { + "status": "failed", + "error": {"code": error_code, "message": message, "detail": detail}, + "message": message, + "updated_at": now, + "completed_at": now, + } + ) + + +def _job_error_from_exception(error: Exception) -> tuple[str, str, Optional[str]]: + if isinstance(error, HTTPException): + message = str(error.detail) + if error.status_code == 507: + return "cuda_out_of_memory", message, None + if error.status_code in {400, 404, 409}: + return "validation_error", message, None + return "backend_error", message, None + if isinstance(error, torch.OutOfMemoryError): + return "cuda_out_of_memory", "CUDA out of memory during operation.", str(error) + if isinstance(error, RuntimeError) and "out of memory" in str(error).lower(): + return "cuda_out_of_memory", "CUDA out of memory during operation.", str(error) + return "backend_error", "Backend operation failed.", str(error) + + +def _run_job(job_id: str, worker: Callable[[], dict[str, Any]]) -> None: + with current_job_lock: + if current_job is None or current_job.get("job_id") != job_id: + return + current_job["status"] = "running" + current_job["updated_at"] = _utc_now_iso() + + try: + result = worker() + _complete_job(result) + except Exception as error: + logger.exception("Background job failed") + error_code, message, detail = _job_error_from_exception(error) + _fail_job(error_code, message, detail) + + +def _queue_long_job( + *, + operation: str, + stage: str, + stage_label: str, + message: str, + worker: Callable[[], dict[str, Any]], +) -> dict[str, Any]: + job = _start_job(operation, stage=stage, stage_label=stage_label, message=message) + thread = threading.Thread(target=_run_job, args=(job["job_id"], worker), daemon=True) + thread.start() + return { + "job_id": job["job_id"], + "status": job["status"], + "operation": job["operation"], + "message": message, + } def _cleanup_cuda_memory(): @@ -108,9 +299,15 @@ def _load_video_frames_as_numpy(video_dir_path: Path, frame_file_names: list[str return np.stack(frames_rgb, axis=0) -def _build_window_dir(frame_paths: list[Path], run_root: Path, window_name: str) -> Path: +def _build_window_dir( + frame_paths: list[Path], + run_root: Path, + window_name: str, + progress_callback: Optional[Callable[[int, int, Path], None]] = None, +) -> Path: window_dir = run_root / window_name window_dir.mkdir(parents=True, exist_ok=True) + total = len(frame_paths) for local_idx, source_path in enumerate(frame_paths): target_name = f"{local_idx:05d}{source_path.suffix.lower()}" @@ -122,6 +319,8 @@ def _build_window_dir(frame_paths: list[Path], run_root: Path, window_name: str) os.link(source_path, target_path) except OSError: shutil.copy2(source_path, target_path) + if progress_callback is not None: + progress_callback(local_idx + 1, total, source_path) return window_dir @@ -295,20 +494,52 @@ def _initialize_video_state_from_resolved_input( global video_masker, video_dir, video_frame_files, video_source_path source_video_path = None + extraction_reported = False if resolved_input_path.is_file(): suffix = resolved_input_path.suffix.lower() if suffix in VIDEO_EXTENSIONS: + def _on_extract_progress(current: int, total: Optional[int]) -> None: + nonlocal extraction_reported + extraction_reported = True + if total: + progress = min(0.7, 0.15 + (0.5 * (current / total))) + message = f"Extracted {current} of {total} frames" + else: + progress = None + message = f"Extracted {current} frames" + _update_job( + stage="extracting_frames", + stage_label="Extracting video frames", + progress=progress, + current=current, + total=total, + frame_idx=max(0, current - 1), + message=message, + append_history=current == 1 or (bool(total) and current == total), + ) + try: resolved_video_dir = extract_video_to_frames( resolved_input_path, output_root=GENERATED_FRAMES_ROOT, image_extensions=IMAGE_EXTENSIONS, + progress_callback=_on_extract_progress, ) except ValueError as error: raise HTTPException(status_code=400, detail=str(error)) from error except Exception as error: raise HTTPException(status_code=500, detail=str(error)) from error source_video_path = str(resolved_input_path) + if not extraction_reported: + _update_job( + stage="indexing_frames", + stage_label="Indexing video frames", + progress=0.7, + current=None, + total=None, + frame_idx=None, + message="Found cached frame directory", + ) else: if suffix in IMAGE_EXTENSIONS: detail = ( @@ -325,6 +556,15 @@ def _initialize_video_state_from_resolved_input( resolved_video_dir = resolved_input_path video_dir = str(resolved_video_dir) + _update_job( + stage="initializing_state", + stage_label="Initializing video state", + progress=0.75, + current=None, + total=None, + frame_idx=None, + message="Initializing SAM2 state", + ) try: video_masker.init_state( video_dir, @@ -337,11 +577,42 @@ def _initialize_video_state_from_resolved_input( except ValueError as error: raise HTTPException(status_code=400, detail=str(error)) from error - video_frame_files = sorted([ - frame_path.name + _update_job( + stage="indexing_frames", + stage_label="Indexing video frames", + progress=0.85, + message="SAM2 state initialized; indexing frame files", + ) + + candidate_paths = sorted([ + frame_path for frame_path in resolved_video_dir.iterdir() - if frame_path.is_file() and frame_path.suffix.lower() in IMAGE_EXTENSIONS + if frame_path.is_file() ]) + total_candidates = len(candidate_paths) + report_every = max(1, total_candidates // 100) if total_candidates else 1 + indexed_frame_files: list[str] = [] + for candidate_idx, frame_path in enumerate(candidate_paths, start=1): + if frame_path.suffix.lower() in IMAGE_EXTENSIONS: + indexed_frame_files.append(frame_path.name) + should_report = ( + candidate_idx == 1 + or candidate_idx == total_candidates + or candidate_idx % report_every == 0 + ) + if should_report: + _update_job( + stage="indexing_frames", + stage_label="Indexing video frames", + progress=0.85 + (0.1 * (candidate_idx / total_candidates)) if total_candidates else 0.95, + current=len(indexed_frame_files), + total=total_candidates, + frame_idx=len(indexed_frame_files) - 1 if indexed_frame_files else None, + message=f"Indexed {len(indexed_frame_files)} of {total_candidates} frame files", + append_history=candidate_idx == 1 or candidate_idx == total_candidates, + ) + + video_frame_files = indexed_frame_files if not video_frame_files: raise HTTPException( @@ -466,6 +737,19 @@ async def health(): return {"status": "ok"} +@app.get("/jobs/current") +async def get_current_job(): + return {"job": _serialize_job()} + + +@app.get("/jobs/{job_id}") +async def get_job(job_id: str): + job = _serialize_job() + if job is None or job.get("job_id") != job_id: + raise HTTPException(status_code=404, detail="Job not found.") + return {"job": job} + + @app.get("/status") async def status(): global video_masker, tracker @@ -480,9 +764,39 @@ async def status(): @app.post("/video/init_state") async def init_video_state(request: VideoInitStateRequest): + return _queue_long_job( + operation="video_init", + stage="resolving_input", + stage_label="Resolving input", + message="Video initialization queued", + worker=lambda: _run_video_init_job(request), + ) + + +def _run_video_init_job(request: VideoInitStateRequest) -> dict[str, Any]: + _update_job( + status="running", + stage="resolving_input", + stage_label="Resolving input", + progress=0.1, + message="Resolving video path", + ) _prepare_video_masker_for_video_init() resolved_input_path = _resolve_input_path(request.video_frames_dir) - return _initialize_video_state_from_resolved_input( + _update_job( + stage="loading_sam2", + stage_label="Loading SAM2", + progress=0.35, + message="Preparing video masker", + ) + if resolved_input_path.is_file() and resolved_input_path.suffix.lower() in VIDEO_EXTENSIONS: + _update_job( + stage="extracting_frames", + stage_label="Extracting frames", + progress=0.45, + message="Extracting video frames", + ) + result = _initialize_video_state_from_resolved_input( resolved_input_path, online_mode=request.online_mode, batch_size=request.batch_size, @@ -490,6 +804,15 @@ async def init_video_state(request: VideoInitStateRequest): offload_state_to_cpu=request.offload_state_to_cpu, async_loading_frames=request.async_loading_frames, ) + _update_job( + stage="indexing_frames", + stage_label="Indexing video frames", + progress=0.95, + current=int(result.get("num_frames", 0)), + total=int(result.get("num_frames", 0)), + message=f"Indexed {int(result.get('num_frames', 0))} frames", + ) + return result @app.post("/video/reset_state") async def reset_video_state(): @@ -636,11 +959,31 @@ async def add_new_mask(request: VideoAddMaskRequest): @app.post("/video/propagate_in_video") async def propagate_in_video(request: VideoPropagateRequest): + return _queue_long_job( + operation="mask_propagation", + stage="validating_prompts", + stage_label="Validating prompts", + message="Mask propagation queued", + worker=lambda: _run_propagation_job(request), + ) + + +def _run_propagation_job(request: VideoPropagateRequest) -> dict[str, Any]: global video_masker, video_dir, video_frame_files, mask_manifest_path, video_state_epoch + def _propagation_progress(processed_frames_count: int, expected_frames_count: int) -> float: + return (processed_frames_count / expected_frames_count) if expected_frames_count else 0.0 + + _update_job( + status="running", + stage="validating_prompts", + stage_label="Validating prompts", + progress=0.0, + message="Validating mask propagation inputs", + ) if video_masker is None: - return {"error": "Video masker not active."} + raise HTTPException(status_code=400, detail="Video masker not active.") if video_dir is None: - return {"error": "Video directory not set. Call /video/init_state first."} + raise HTTPException(status_code=400, detail="Video directory not set. Call /video/init_state first.") if request.reverse: raise HTTPException( @@ -692,6 +1035,16 @@ async def propagate_in_video(request: VideoPropagateRequest): if end_frame_idx < start_frame_idx: raise HTTPException(status_code=400, detail="Invalid propagation frame range.") + expected_total_frames = end_frame_idx - start_frame_idx + 1 + _update_job( + stage="preparing_manifest", + stage_label="Preparing manifest", + progress=0.02, + current=0, + total=expected_total_frames, + message=f"Preparing masks for {expected_total_frames} frames", + ) + frame_files, masks_dir = prepare_video_masks_output(video_dir) manifest_file_path = masks_dir / "manifest.json" @@ -727,9 +1080,55 @@ async def propagate_in_video(request: VideoPropagateRequest): try: for window_index, (window_start, window_end) in enumerate(windows): + window_number = window_index + 1 + window_count = len(windows) + _update_job( + stage="building_window", + stage_label="Loading propagation window", + current=0, + total=window_end - window_start + 1, + window_index=window_number, + window_count=window_count, + frame_idx=window_start, + progress=_propagation_progress(len(processed_frames), expected_total_frames), + message=f"Preparing window {window_number} of {window_count}: frames {window_start}-{window_end}", + ) window_frame_paths = [Path(video_dir) / video_frame_files[idx] for idx in range(window_start, window_end + 1)] window_name = f"window_{window_index}_{window_start}_{window_end}" - window_dir = _build_window_dir(window_frame_paths, run_root, window_name) + + def _on_window_build_progress(current: int, total: int, source_path: Path) -> None: + source_frame_idx = window_start + current - 1 + _update_job( + stage="building_window", + stage_label="Loading propagation window", + current=current, + total=total, + window_index=window_number, + window_count=window_count, + frame_idx=source_frame_idx, + progress=_propagation_progress(len(processed_frames), expected_total_frames), + message=f"Linked {current} of {total} frames for window {window_number} of {window_count}", + append_history=current == 1 or current == total, + ) + + window_dir = _build_window_dir( + window_frame_paths, + run_root, + window_name, + progress_callback=_on_window_build_progress, + ) + + _update_job( + stage="initializing_state", + stage_label="Initializing SAM2 window state", + current=len(processed_frames), + total=expected_total_frames, + window_index=window_number, + window_count=window_count, + frame_idx=window_start, + progress=_propagation_progress(len(processed_frames), expected_total_frames), + message=f"Initializing SAM2 state for window {window_number} of {window_count}", + ) video_masker.init_state( str(window_dir), @@ -740,7 +1139,30 @@ async def propagate_in_video(request: VideoPropagateRequest): async_loading_frames=False, ) + _update_job( + stage="seeding_window", + stage_label="Seeding prompts", + current=len(processed_frames), + total=expected_total_frames, + window_index=window_number, + window_count=window_count, + frame_idx=window_start, + progress=_propagation_progress(len(processed_frames), expected_total_frames), + message=f"Seeding prompts for window {window_number} of {window_count}", + ) + if window_index > 0 and boundary_masks and boundary_frame_idx is not None: + _update_job( + stage="seeding_window", + stage_label="Seeding boundary masks", + current=len(processed_frames), + total=expected_total_frames, + window_index=window_number, + window_count=window_count, + frame_idx=boundary_frame_idx, + progress=_propagation_progress(len(processed_frames), expected_total_frames), + message=f"Seeding boundary masks for window {window_number} of {window_count}", + ) local_boundary_idx = int(boundary_frame_idx - window_start) for obj_id, obj_mask in boundary_masks.items(): video_masker.add_new_mask( @@ -788,6 +1210,18 @@ def _on_window_frame(local_frame_idx: int, frame_masks: dict[int, np.ndarray]): return processed_frames.add(global_frame_idx) + _update_job( + stage="propagating_window", + stage_label="Propagating masks", + current=len(processed_frames), + total=expected_total_frames, + window_index=window_number, + window_count=window_count, + frame_idx=global_frame_idx, + progress=_propagation_progress(len(processed_frames), expected_total_frames), + message=f"Processed {len(processed_frames)} of {expected_total_frames} frames", + append_history=len(processed_frames) == 1 or len(processed_frames) == expected_total_frames, + ) manifest_frames[str(global_frame_idx)] = _manifest_frame_payload(frame_masks) if request.include_masks_in_response: video_segments_serializable[global_frame_idx] = { @@ -826,6 +1260,17 @@ def _on_window_frame(local_frame_idx: int, frame_masks: dict[int, np.ndarray]): collect_segments=False, frame_callback=_on_window_frame, ) + _update_job( + stage="propagating_window", + stage_label="Propagating masks", + current=len(processed_frames), + total=expected_total_frames, + window_index=window_number, + window_count=window_count, + frame_idx=window_end, + progress=_propagation_progress(len(processed_frames), expected_total_frames), + message=f"Finished window {window_number} of {window_count}", + ) except ValueError as error: raise HTTPException(status_code=400, detail=str(error)) from error except torch.OutOfMemoryError as error: @@ -849,9 +1294,31 @@ def _on_window_frame(local_frame_idx: int, frame_masks: dict[int, np.ndarray]): write_mask_manifest(manifest_file_path, manifest) mask_manifest_path = str(manifest_file_path) + _update_job( + stage="saving_manifest", + stage_label="Saving manifest", + progress=0.98, + current=len(processed_frames), + total=expected_total_frames, + window_index=None, + window_count=None, + frame_idx=None, + message="Saving mask manifest", + ) try: # Rebind interactive state to the original full video frame index space. + _update_job( + stage="restoring_state", + stage_label="Restoring interactive state", + progress=0.99, + current=len(processed_frames), + total=expected_total_frames, + window_index=None, + window_count=None, + frame_idx=None, + message="Restoring interactive masking state", + ) _restore_video_masker_from_prompt_events( online_mode=effective_online_mode, batch_size=effective_batch_size, @@ -945,7 +1412,24 @@ async def load_tracking_video(request: TrackingLoadVideoRequest): @app.post("/tracking/track_prompt_points") async def track_prompt_points(request: TrackingPromptPointsRequest): + return _queue_long_job( + operation="prompt_tracking", + stage="collecting_prompts", + stage_label="Collecting prompts", + message="Prompt tracking queued", + worker=lambda: _run_prompt_tracking_job(request), + ) + + +def _run_prompt_tracking_job(request: TrackingPromptPointsRequest) -> dict[str, Any]: global tracker, tracking_video, tracking_video_path, video_masker, video_prompt_events, video_dir, video_state_epoch + _update_job( + status="running", + stage="collecting_prompts", + stage_label="Collecting prompts", + progress=0.05, + message="Collecting positive prompt points", + ) if not video_prompt_events: raise HTTPException(status_code=400, detail="No annotation prompts available for tracking.") @@ -978,6 +1462,15 @@ async def track_prompt_points(request: TrackingPromptPointsRequest): if not positive_queries: raise HTTPException(status_code=400, detail="No positive prompt points available for tracking.") + total_queries = len(positive_queries) + _update_job( + stage="loading_tracker", + stage_label="Loading tracker", + progress=0.2, + current=0, + total=total_queries, + message=f"Preparing to track {total_queries} prompt points", + ) should_restore_video_masker = video_masker is not None and video_dir is not None restore_online_mode = video_masker.online_mode if video_masker is not None else True @@ -1010,6 +1503,14 @@ def _restore_masker_state(*, raise_on_error: bool) -> None: _cleanup_cuda_memory() _ensure_tracker_model(request.model_name) + _update_job( + stage="loading_frames", + stage_label="Loading frames", + progress=0.35, + current=0, + total=total_queries, + message="Loading video frames for tracking", + ) def _is_oom_runtime_error(error: RuntimeError) -> bool: return "out of memory" in str(error).lower() @@ -1060,6 +1561,14 @@ def _track_queries_batched( tracks_batches.append(batch_tracks) visibility_batches.append(batch_visibility) index = end_index + _update_job( + stage="tracking_points", + stage_label="Tracking prompt points", + progress=0.35 + (0.55 * (index / query_array.shape[0])), + current=index, + total=query_array.shape[0], + message=f"Tracked {index} of {query_array.shape[0]} prompt points", + ) tracks = np.concatenate(tracks_batches, axis=0) visibility = np.concatenate(visibility_batches, axis=0) @@ -1119,6 +1628,14 @@ def _track_queries_batched( raise HTTPException(status_code=500, detail=f"Prompt-point tracking failed: {error}") from error _cleanup_cuda_memory() + _update_job( + stage="restoring_masker", + stage_label="Restoring masking state", + progress=0.95, + current=int(tracks.shape[0]), + total=total_queries, + message="Restoring interactive masking state", + ) _restore_masker_state(raise_on_error=True) return { diff --git a/backend/tests/tester.py b/backend/tests/tester.py index d3b119a..89a21f8 100755 --- a/backend/tests/tester.py +++ b/backend/tests/tester.py @@ -54,6 +54,77 @@ def _env_bool(name: str, default: bool) -> bool: ONLINE_BATCH_SIZE = int(os.getenv("DATA_ENGINE_BATCH_SIZE", "32")) OFFLOAD_VIDEO_TO_CPU = _env_bool("DATA_ENGINE_OFFLOAD_VIDEO_TO_CPU", True) OFFLOAD_STATE_TO_CPU = _env_bool("DATA_ENGINE_OFFLOAD_STATE_TO_CPU", False) +JOB_POLL_INTERVAL_SECONDS = float(os.getenv("DATA_ENGINE_JOB_POLL_INTERVAL", "0.5")) +JOB_TIMEOUT_SECONDS = float(os.getenv("DATA_ENGINE_JOB_TIMEOUT", "1800")) + + +def _print_job_progress(job): + stage_label = job.get("stage_label") or job.get("stage") or "Working" + progress = job.get("progress") + current = job.get("current") + total = job.get("total") + window_index = job.get("window_index") + window_count = job.get("window_count") + frame_idx = job.get("frame_idx") + history = job.get("stage_history") or [] + message = job.get("message") or "" + + parts = [stage_label] + if window_index is not None and window_count: + parts.append(f"Window {window_index}/{window_count}") + if frame_idx is not None: + parts.append(f"Frame {frame_idx}") + if isinstance(progress, (int, float)): + parts.append(f"{progress * 100:5.1f}%") + if current is not None and total: + parts.append(f"{current}/{total}") + if message: + parts.append(message) + if history: + latest_history_message = history[-1].get("message") + if latest_history_message and latest_history_message != message: + parts.append(f"last: {latest_history_message}") + + print("\r " + " | ".join(parts), end="", flush=True) + + +def wait_for_job(job_start_response, timeout=JOB_TIMEOUT_SECONDS): + """Polls a background job until it completes and returns its result payload.""" + job_id = job_start_response.get("job_id") + if not job_id: + print(f"Invalid job start response: {job_start_response}") + return False, None + + started = time.time() + last_status = None + while time.time() - started < timeout: + response = requests.get(f"{BASE_URL}/jobs/{job_id}") + if response.status_code != 200: + print(f"\nFailed to poll job {job_id}. Status: {response.status_code}, Response: {response.text}") + return False, None + + job = response.json().get("job", {}) + status = job.get("status") + if status != last_status: + print(f"\n Job {job_id}: {status}") + last_status = status + _print_job_progress(job) + + if status == "completed": + print() + return True, job.get("result") + if status == "failed": + print() + error = job.get("error") or {} + print(f"Job failed: {error.get('message', 'Unknown error')}") + if error.get("detail"): + print(f" Detail: {error.get('detail')}") + return False, job + + time.sleep(JOB_POLL_INTERVAL_SECONDS) + + print(f"\nTimed out waiting for job {job_id} after {timeout:.1f}s.") + return False, None def download_and_extract_bedroom(): @@ -133,10 +204,14 @@ def init_video_state(video_dir): } response = requests.post(url, json=payload) if response.status_code == 200: - print(f"Video state initialized successfully for '{video_dir}'.") + print(f"Video init job started for '{video_dir}'.") + success, result = wait_for_job(response.json()) + if success: + print(f"Video state initialized successfully for '{video_dir}'.") + return success else: print(f"Failed to initialize video state. Status: {response.status_code}, Response: {response.text}") - return response.status_code == 200 + return False def reset_video_state(): @@ -187,7 +262,10 @@ def propagate_in_video(start_frame_idx=None, max_frame_num_to_track=None, revers } response = requests.post(url, json=payload) if response.status_code == 200: - response_data = response.json() + print("Propagation job started.") + success, response_data = wait_for_job(response.json()) + if not success or response_data is None: + return False, response_data online_mode = response_data.get("online_mode") batch_size = response_data.get("batch_size") if online_mode is not None: @@ -201,9 +279,10 @@ def propagate_in_video(start_frame_idx=None, max_frame_num_to_track=None, revers print(f"Saved masks for {len(saved_paths)} frames.") for frame_idx, paths in saved_paths.items(): print(f" Frame {frame_idx}: {paths}") + return True, response_data else: print(f"Failed to propagate. Status: {response.status_code}, Response: {response.text}") - return response.status_code == 200, response.json() if response.status_code == 200 else None + return False, None def stop_server_process(server_process): diff --git a/backend/utils.py b/backend/utils.py index 2f316f4..2e71143 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -4,7 +4,7 @@ import shutil import hashlib import json -from typing import Any +from typing import Any, Callable, Optional def _color_from_obj_id(obj_id): @@ -248,7 +248,12 @@ def save_single_video_mask_frame(frame_files, masks_dir, frame_idx, obj_masks): return str(output_path) -def extract_video_to_frames(video_path: Path, output_root: Path, image_extensions: set[str] | None = None) -> Path: +def extract_video_to_frames( + video_path: Path, + output_root: Path, + image_extensions: set[str] | None = None, + progress_callback: Optional[Callable[[int, Optional[int]], None]] = None, +) -> Path: """ Extract a video into a cached frame directory. @@ -256,6 +261,7 @@ def extract_video_to_frames(video_path: Path, output_root: Path, image_extension - video_path: path to the source video file - output_root: root directory where extracted frame folders are stored - image_extensions: frame extensions considered valid for cache checks + - progress_callback: optional callback receiving extracted count and total frames when known Returns: Path to directory containing extracted frame images """ @@ -285,6 +291,8 @@ def extract_video_to_frames(video_path: Path, output_root: Path, image_extension if not capture.isOpened(): raise ValueError(f"Unable to open video file: {video_path}") + raw_total_frames = int(capture.get(cv2.CAP_PROP_FRAME_COUNT) or 0) + total_frames: Optional[int] = raw_total_frames if raw_total_frames > 0 else None frame_idx = 0 try: while True: @@ -295,6 +303,8 @@ def extract_video_to_frames(video_path: Path, output_root: Path, image_extension if not cv2.imwrite(str(output_path), frame): raise RuntimeError(f"Failed to write extracted frame: {output_path}") frame_idx += 1 + if progress_callback is not None: + progress_callback(frame_idx, total_frames) finally: capture.release() diff --git a/frontend-ng/data-engine/angular.json b/frontend-ng/data-engine/angular.json index 30a2f80..ca58ffa 100644 --- a/frontend-ng/data-engine/angular.json +++ b/frontend-ng/data-engine/angular.json @@ -39,8 +39,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "4kB", - "maximumError": "8kB" + "maximumWarning": "6kB", + "maximumError": "10kB" } ], "outputHashing": "all" diff --git a/frontend-ng/data-engine/package-lock.json b/frontend-ng/data-engine/package-lock.json index 5f87654..a8258a5 100644 --- a/frontend-ng/data-engine/package-lock.json +++ b/frontend-ng/data-engine/package-lock.json @@ -14,6 +14,8 @@ "@angular/forms": "^21.0.0", "@angular/platform-browser": "^21.0.0", "@angular/router": "^21.0.0", + "@tauri-apps/api": "^2.0.0", + "@tauri-apps/plugin-dialog": "^2.0.0", "rxjs": "~7.8.0", "tslib": "^2.3.0" }, @@ -22,6 +24,7 @@ "@angular/cli": "^21.0.0", "@angular/compiler-cli": "^21.0.0", "@tailwindcss/postcss": "^4.1.12", + "@tauri-apps/cli": "^2.0.0", "jsdom": "^27.1.0", "postcss": "^8.5.3", "tailwindcss": "^4.1.12", @@ -476,7 +479,6 @@ "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.0.0.tgz", "integrity": "sha512-uFvQDYU5X5nEnI9C4Bkdxcu4aIzNesGLJzmFlnwChVxB4BxIRF0uHL0oRhdkInGTIzPDJPH4nF6B/22c5gDVqA==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -493,7 +495,6 @@ "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.0.0.tgz", "integrity": "sha512-6jCH3UYga5iokj5F40SR4dlwo9ZRMkT8YzHCTijwZuDX9zvugp9jPof092RvIeNsTvCMVfGWuM9yZ1DRUsU/yg==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -507,7 +508,6 @@ "integrity": "sha512-KTXp+e2UPGyfFew6Wq95ULpHWQ20dhqkAMZ6x6MCYfOe2ccdnGYsAbLLmnWGmSg5BaOI4B0x/1XCFZf/n6WDgA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "7.28.4", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -540,7 +540,6 @@ "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.0.0.tgz", "integrity": "sha512-bqi8fT4csyITeX8vdN5FJDBWx5wuWzdCg4mKSjHd+onVzZLyZ8bcnuAKz4mklgvjvwuXoRYukmclUurLwfq3Rg==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -585,7 +584,6 @@ "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.0.0.tgz", "integrity": "sha512-KQrANla4RBLhcGkwlndqsKzBwVFOWQr1640CfBVjj2oz4M3dW5hyMtXivBACvuwyUhYU/qJbqlDMBXl/OUSudQ==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -1057,7 +1055,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -1101,7 +1098,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -1818,7 +1814,6 @@ "integrity": "sha512-X7/+dG9SLpSzRkwgG5/xiIzW0oMrV3C0HOa7YHG1WnrLK+vCQHfte4k/T80059YBdei29RBC3s+pSMvPJDU9/A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@inquirer/checkbox": "^4.3.0", "@inquirer/confirm": "^5.1.19", @@ -3970,8 +3965,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@tailwindcss/node": { "version": "4.1.17", @@ -4254,6 +4248,257 @@ "tailwindcss": "4.1.17" } }, + "node_modules/@tauri-apps/api": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.10.1.tgz", + "integrity": "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==", + "license": "Apache-2.0 OR MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.10.1.tgz", + "integrity": "sha512-jQNGF/5quwORdZSSLtTluyKQ+o6SMa/AUICfhf4egCGFdMHqWssApVgYSbg+jmrZoc8e1DscNvjTnXtlHLS11g==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.10.1", + "@tauri-apps/cli-darwin-x64": "2.10.1", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.10.1", + "@tauri-apps/cli-linux-arm64-gnu": "2.10.1", + "@tauri-apps/cli-linux-arm64-musl": "2.10.1", + "@tauri-apps/cli-linux-riscv64-gnu": "2.10.1", + "@tauri-apps/cli-linux-x64-gnu": "2.10.1", + "@tauri-apps/cli-linux-x64-musl": "2.10.1", + "@tauri-apps/cli-win32-arm64-msvc": "2.10.1", + "@tauri-apps/cli-win32-ia32-msvc": "2.10.1", + "@tauri-apps/cli-win32-x64-msvc": "2.10.1" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.10.1.tgz", + "integrity": "sha512-Z2OjCXiZ+fbYZy7PmP3WRnOpM9+Fy+oonKDEmUE6MwN4IGaYqgceTjwHucc/kEEYZos5GICve35f7ZiizgqEnQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.10.1.tgz", + "integrity": "sha512-V/irQVvjPMGOTQqNj55PnQPVuH4VJP8vZCN7ajnj+ZS8Kom1tEM2hR3qbbIRoS3dBKs5mbG8yg1WC+97dq17Pw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.10.1.tgz", + "integrity": "sha512-Hyzwsb4VnCWKGfTw+wSt15Z2pLw2f0JdFBfq2vHBOBhvg7oi6uhKiF87hmbXOBXUZaGkyRDkCHsdzJcIfoJC2w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.10.1.tgz", + "integrity": "sha512-OyOYs2t5GkBIvyWjA1+h4CZxTcdz1OZPCWAPz5DYEfB0cnWHERTnQ/SLayQzncrT0kwRoSfSz9KxenkyJoTelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.10.1.tgz", + "integrity": "sha512-MIj78PDDGjkg3NqGptDOGgfXks7SYJwhiMh8SBoZS+vfdz7yP5jN18bNaLnDhsVIPARcAhE1TlsZe/8Yxo2zqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.10.1.tgz", + "integrity": "sha512-X0lvOVUg8PCVaoEtEAnpxmnkwlE1gcMDTqfhbefICKDnOTJ5Est3qL0SrWxizDackIOKBcvtpejrSiVpuJI1kw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.10.1.tgz", + "integrity": "sha512-2/12bEzsJS9fAKybxgicCDFxYD1WEI9kO+tlDwX5znWG2GwMBaiWcmhGlZ8fi+DMe9CXlcVarMTYc0L3REIRxw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.10.1.tgz", + "integrity": "sha512-Y8J0ZzswPz50UcGOFuXGEMrxbjwKSPgXftx5qnkuMs2rmwQB5ssvLb6tn54wDSYxe7S6vlLob9vt0VKuNOaCIQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.10.1.tgz", + "integrity": "sha512-iSt5B86jHYAPJa/IlYw++SXtFPGnWtFJriHn7X0NFBVunF6zu9+/zOn8OgqIWSl8RgzhLGXQEEtGBdR4wzpVgg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.10.1.tgz", + "integrity": "sha512-gXyxgEzsFegmnWywYU5pEBURkcFN/Oo45EAwvZrHMh+zUSEAvO5E8TXsgPADYm31d1u7OQU3O3HsYfVBf2moHw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.10.1.tgz", + "integrity": "sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/plugin-dialog": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.7.0.tgz", + "integrity": "sha512-4nS/hfGMGCXiAS3LtVjH9AgsSAPJeG/7R+q8agTFqytjnMa4Zq95Bq8WzVDkckpanX+yyRHXnRtrKXkANKDHvw==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.10.1" + } + }, "node_modules/@tufjs/canonical-json": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", @@ -4760,7 +5005,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -4950,7 +5194,6 @@ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "readdirp": "^4.0.1" }, @@ -5676,7 +5919,6 @@ "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", @@ -6431,7 +6673,6 @@ "integrity": "sha512-454TI39PeRDW1LgpyLPyURtB4Zx1tklSr6+OFOipsxGUH1WMTvk6C65JQdrj455+DP2uJ1+veBEHTGFKWVLFoA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@acemir/cssom": "^0.9.23", "@asamuzakjp/dom-selector": "^6.7.4", @@ -8280,7 +8521,6 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -8838,8 +9078,7 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tapable": { "version": "2.3.0", @@ -8997,8 +9236,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tuf-js": { "version": "4.0.0", @@ -9036,7 +9274,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9169,7 +9406,6 @@ "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -9729,7 +9965,6 @@ "integrity": "sha512-pmW4GCKQ8t5Ko1jYjC3SqOr7TUKN7uHOHB/XGsAIb69eYu6d1ionGSsb5H9chmPf+WeXt0VE7jTXsB1IvWoNbw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "4.0.12", "@vitest/mocker": "4.0.12", @@ -10254,7 +10489,6 @@ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/frontend-ng/data-engine/src/app/services/backend.service.spec.ts b/frontend-ng/data-engine/src/app/services/backend.service.spec.ts index 062d8b2..5a6dbf2 100644 --- a/frontend-ng/data-engine/src/app/services/backend.service.spec.ts +++ b/frontend-ng/data-engine/src/app/services/backend.service.spec.ts @@ -63,4 +63,28 @@ describe('BackendService', () => { expect(service.getApiUrl()).toBe('http://127.0.0.1:8000'); expect(localStorage.getItem('dataEngineApiUrl')).toBeNull(); }); + + it('calls the health endpoint', () => { + const http = { + post: vi.fn(), + get: vi.fn(() => of({ status: 'ok' })), + } as unknown as HttpClient; + const service = new BackendService(http); + + service.health().subscribe(); + + expect((http.get as any).mock.calls[0][0]).toBe('http://127.0.0.1:8000/health'); + }); + + it('fetches job status by id', () => { + const http = { + post: vi.fn(), + get: vi.fn(() => of({ job: null })), + } as unknown as HttpClient; + const service = new BackendService(http); + + service.getJob('abc123').subscribe(); + + expect((http.get as any).mock.calls[0][0]).toBe('http://127.0.0.1:8000/jobs/abc123'); + }); }); diff --git a/frontend-ng/data-engine/src/app/services/backend.service.ts b/frontend-ng/data-engine/src/app/services/backend.service.ts index b9500dc..27702fc 100644 --- a/frontend-ng/data-engine/src/app/services/backend.service.ts +++ b/frontend-ng/data-engine/src/app/services/backend.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; -import { Observable } from 'rxjs'; +import { Observable, timeout } from 'rxjs'; const API_URL_STORAGE_KEY = 'dataEngineApiUrl'; const DEFAULT_API_URL = 'http://127.0.0.1:8000'; @@ -112,6 +112,64 @@ export interface TrackPromptPointsResponse { state_epoch?: number; } +export type ApiHealthStatus = 'checking' | 'online' | 'offline'; +export type JobStatus = 'queued' | 'running' | 'completed' | 'failed'; +export type JobOperation = 'video_init' | 'mask_propagation' | 'prompt_tracking'; + +export interface HealthResponse { + status: string; +} + +export interface JobStartResponse { + job_id: string; + status: JobStatus; + operation: JobOperation; + message: string; +} + +export interface JobError { + code: string; + message: string; + detail?: string | null; +} + +export interface JobStageHistoryEntry { + stage: string; + stage_label: string; + message: string; + progress: number | null; + updated_at: string; +} + +export interface BackendJob { + job_id: string; + operation: JobOperation; + status: JobStatus; + stage: string; + stage_label: string; + progress: number | null; + current: number | null; + total: number | null; + window_index: number | null; + window_count: number | null; + frame_idx: number | null; + stage_history?: JobStageHistoryEntry[]; + message: string; + result: T | null; + error: JobError | null; + started_at: string; + updated_at: string; + completed_at: string | null; +} + +export interface JobResponse { + job: BackendJob; +} + +export interface CurrentJobResponse { + job: BackendJob | null; +} + @Injectable({ providedIn: 'root' }) @@ -172,6 +230,18 @@ export class BackendService { return this.apiUrl; } + health(): Observable { + return this.http.get(this.endpoint('/health')).pipe(timeout(2000)); + } + + getCurrentJob(): Observable { + return this.http.get(this.endpoint('/jobs/current')); + } + + getJob(jobId: string): Observable> { + return this.http.get>(this.endpoint(`/jobs/${jobId}`)); + } + private safeDecodeURIComponent(value: string): string { try { return decodeURIComponent(value); @@ -209,12 +279,12 @@ export class BackendService { initVideoState( dir: string, options?: Omit - ): Observable { + ): Observable { const payload: VideoInitStateRequest = { video_frames_dir: this.normalizePath(dir), ...options }; - return this.http.post(this.endpoint('/video/init_state'), payload); + return this.http.post(this.endpoint('/video/init_state'), payload); } resetVideoState(): Observable { @@ -225,8 +295,8 @@ export class BackendService { return this.http.post(this.endpoint('/video/add_new_points_or_box'), request); } - propagateInVideo(request: VideoPropagateRequest): Observable { - return this.http.post(this.endpoint('/video/propagate_in_video'), request); + propagateInVideo(request: VideoPropagateRequest): Observable { + return this.http.post(this.endpoint('/video/propagate_in_video'), request); } clearAllPromptsInFrame(frameIdx: number, objId: number): Observable { @@ -257,7 +327,7 @@ export class BackendService { return this.http.get(this.endpoint(`/video/mask_data/${frameIdx}`)); } - trackPromptPoints(request: TrackPromptPointsRequest): Observable { - return this.http.post(this.endpoint('/tracking/track_prompt_points'), request); + trackPromptPoints(request: TrackPromptPointsRequest): Observable { + return this.http.post(this.endpoint('/tracking/track_prompt_points'), request); } } diff --git a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.css b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.css index 22838a7..8ed6cee 100644 --- a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.css +++ b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.css @@ -26,6 +26,7 @@ flex-wrap: wrap; gap: 8px; align-items: center; + margin-left: auto; } .api-url-input { @@ -33,6 +34,35 @@ min-width: 220px; } +.api-status-pill { + border: 1px solid #bbb; + border-radius: 4px; + padding: 5px 8px; + background: #fff; + color: #555; + font-size: 0.82rem; + font-weight: 600; + line-height: 1.1; +} + +.api-status-pill.online { + border-color: #8fbd8f; + background: #f1f8f1; + color: #246b24; +} + +.api-status-pill.offline { + border-color: #d8a0a0; + background: #fff5f5; + color: #9b2c2c; +} + +.api-status-pill.checking { + border-color: #bbb; + background: #f7f7f7; + color: #555; +} + .main-area { flex-grow: 1; display: flex; @@ -194,7 +224,7 @@ button:disabled { left: 0; width: 100%; height: 100%; - background-color: rgba(0, 0, 0, 0.5); + background-color: rgba(0, 0, 0, 0.35); display: flex; flex-direction: column; justify-content: center; @@ -203,28 +233,165 @@ button:disabled { color: white; } -.loading-spinner { - border: 4px solid #f3f3f3; - border-top: 4px solid #3498db; - border-radius: 50%; - width: 40px; - height: 40px; - animation: spin 1s linear infinite; +.progress-panel { + width: min(520px, calc(100vw - 32px)); + padding: 14px; + border: 1px solid #ccc; + border-radius: 4px; + background: #ffffff; + color: #222; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18); +} + +.progress-title { + font-size: 1.05em; + font-weight: 700; + margin-bottom: 6px; +} + +.progress-stage { + color: #444; + font-size: 0.9rem; + margin-bottom: 8px; +} + +.progress-context { + display: flex; + flex-wrap: wrap; + gap: 6px; margin-bottom: 10px; + color: #555; + font-size: 0.82rem; +} + +.progress-context span { + border: 1px solid #ddd; + border-radius: 4px; + background: #f8f8f8; + padding: 3px 6px; +} + +.progress-track { + position: relative; + height: 9px; + overflow: hidden; + border: 1px solid #ccc; + border-radius: 4px; + background: #f0f0f0; +} + +.progress-fill { + height: 100%; + border-radius: inherit; + background: #4d8f4d; + transition: width 180ms ease; +} + +.progress-track.indeterminate .progress-fill { + width: 35% !important; + animation: indeterminate-progress 1.1s ease-in-out infinite; +} + +.progress-meta { + display: flex; + justify-content: space-between; + min-height: 20px; + margin-top: 8px; + color: #444; + font-size: 0.85rem; + font-weight: 600; +} + +.progress-message { + margin-top: 6px; + color: #555; + font-size: 0.86rem; +} + +.progress-history { + margin-top: 10px; + padding-top: 8px; + border-top: 1px solid #e0e0e0; + color: #666; + font-size: 0.8rem; + line-height: 1.35; +} + +.progress-history-title { + margin-bottom: 4px; + color: #444; + font-weight: 700; +} + +.toast-stack { + position: fixed; + right: 16px; + bottom: 16px; + z-index: 1100; + display: flex; + flex-direction: column; + gap: 10px; + width: min(420px, calc(100vw - 32px)); +} + +.toast { + display: flex; + gap: 12px; + justify-content: space-between; + align-items: flex-start; + padding: 10px 12px; + border: 1px solid #ccc; + border-left: 4px solid #888; + border-radius: 4px; + background: #ffffff; + color: #222; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.14); +} + +.toast p { + margin: 4px 0 0; + color: #555; + font-size: 0.86rem; + line-height: 1.35; +} + +.toast button { + border: 1px solid #ccc; + border-radius: 4px; + background: #f5f5f5; + color: #444; + font-size: 0.9rem; + line-height: 1; + padding: 2px 6px; +} + +.toast.error { + border-left-color: #b94a48; + background: #fff8f8; +} + +.toast.warning { + border-left-color: #c28b27; + background: #fffaf0; +} + +.toast.info { + border-left-color: #4f81bd; + background: #f8fbff; } -.loading-text { - font-size: 1.2em; - font-weight: bold; +.toast.success { + border-left-color: #4d8f4d; + background: #f8fff8; } -@keyframes spin { +@keyframes indeterminate-progress { 0% { - transform: rotate(0deg); + transform: translateX(-120%); } 100% { - transform: rotate(360deg); + transform: translateX(320%); } } diff --git a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.html b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.html index 9526491..5238d00 100644 --- a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.html +++ b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.html @@ -6,6 +6,9 @@
+ + {{ apiHealthStatus() === 'online' ? 'Online' : apiHealthStatus() === 'offline' ? 'Offline' : 'Checking' }} + Sync Debug
-
-
Processing...
+
+
{{ activeJobTitle() || 'Processing' }}
+
{{ activeJob()?.stage_label || 'Starting operation' }}
+
+ Window {{ activeJob()?.window_index }} of {{ activeJob()?.window_count }} + Frame {{ activeJob()?.frame_idx }} +
+
+
+
+
+ {{ ((activeJob()?.progress || 0) * 100) | number:'1.0-0' }}% + {{ activeJob()?.current }} / {{ activeJob()?.total }} +
+
{{ activeJob()?.message || 'Waiting for backend job status...' }}
+
+
Recent activity
+
{{ entry.message }}
+
+
+
+ +
+
+
+ {{ toast.title }} +

{{ toast.message }}

+
+ +
diff --git a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.spec.ts b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.spec.ts index 0b18aa4..dd74f8f 100644 --- a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.spec.ts +++ b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.spec.ts @@ -9,6 +9,9 @@ describe('VideoMaskerComponent sync contract', () => { let component: VideoMaskerComponent; let backendMock: { addNewPointsOrBox: ReturnType; + health: ReturnType; + initVideoState: ReturnType; + getJob: ReturnType; getApiUrl: ReturnType; setApiUrl: ReturnType; resetApiUrl: ReturnType; @@ -34,6 +37,9 @@ describe('VideoMaskerComponent sync contract', () => { beforeEach(async () => { backendMock = { addNewPointsOrBox: vi.fn(), + health: vi.fn(() => of({ status: 'ok' })), + initVideoState: vi.fn(), + getJob: vi.fn(), getApiUrl: vi.fn(() => 'http://127.0.0.1:8000'), setApiUrl: vi.fn((value: string) => value), resetApiUrl: vi.fn(() => 'http://127.0.0.1:8000'), @@ -51,6 +57,9 @@ describe('VideoMaskerComponent sync contract', () => { provide: BackendService, useValue: { addNewPointsOrBox: backendMock.addNewPointsOrBox, + health: backendMock.health, + initVideoState: backendMock.initVideoState, + getJob: backendMock.getJob, getApiUrl: backendMock.getApiUrl, setApiUrl: backendMock.setApiUrl, resetApiUrl: backendMock.resetApiUrl, @@ -144,4 +153,92 @@ describe('VideoMaskerComponent sync contract', () => { expect(pickerSpy).toHaveBeenCalled(); }); + + it('starts a video init job, polls completion, and applies the result', async () => { + backendMock.initVideoState.mockReturnValue(of({ + job_id: 'job-1', + status: 'queued', + operation: 'video_init', + message: 'queued', + })); + backendMock.getJob.mockReturnValue(of({ + job: { + job_id: 'job-1', + operation: 'video_init', + status: 'completed', + stage: 'completed', + stage_label: 'Completed', + progress: 1, + current: 12, + total: 12, + window_index: null, + window_count: null, + frame_idx: null, + stage_history: [], + message: 'done', + error: null, + started_at: 'now', + updated_at: 'now', + completed_at: 'now', + result: { + message: 'Video state initialized successfully', + num_frames: 12, + resolved_video_frames_dir: 'C:/frames', + source_video_path: null, + online_mode: true, + batch_size: 32, + offload_video_to_cpu: true, + offload_state_to_cpu: true, + state_epoch: 7, + }, + }, + })); + component.videoDir.set('C:/frames'); + + await component.initVideo(); + + expect(backendMock.initVideoState).toHaveBeenCalledWith('C:/frames'); + expect(backendMock.getJob).toHaveBeenCalledWith('job-1'); + expect(component.isInitialized()).toBe(true); + expect(component.numFrames()).toBe(12); + expect(component.stateEpoch()).toBe(7); + }); + + it('creates an error toast when a job fails', async () => { + backendMock.initVideoState.mockReturnValue(of({ + job_id: 'job-2', + status: 'queued', + operation: 'video_init', + message: 'queued', + })); + backendMock.getJob.mockReturnValue(of({ + job: { + job_id: 'job-2', + operation: 'video_init', + status: 'failed', + stage: 'initializing_state', + stage_label: 'Initializing video state', + progress: 0.5, + current: null, + total: null, + window_index: null, + window_count: null, + frame_idx: null, + stage_history: [], + message: 'failed', + error: { code: 'validation_error', message: 'Path not found', detail: null }, + started_at: 'now', + updated_at: 'now', + completed_at: 'now', + result: null, + }, + })); + component.videoDir.set('C:/missing'); + + await component.initVideo(); + + expect(component.isInitialized()).toBe(false); + expect(component.toasts()[0].title).toBe('Loading video'); + expect(component.toasts()[0].message).toBe('Path not found'); + }); }); diff --git a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.ts b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.ts index bbb33f5..2196f4f 100644 --- a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.ts +++ b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.ts @@ -1,12 +1,18 @@ -import { Component, ElementRef, ViewChild, effect, signal } from '@angular/core'; +import { Component, ElementRef, OnDestroy, ViewChild, effect, signal } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { firstValueFrom } from 'rxjs'; import { + ApiHealthStatus, + BackendJob, BackendService, + JobStageHistoryEntry, TrackPromptPointMetadata, + TrackPromptPointsResponse, VideoAddPointsOrBoxRequest, - VideoMaskObjectData + VideoInitStateResponse, + VideoMaskObjectData, + VideoPropagateResponse } from '../services/backend.service'; import { DesktopBridgeService } from '../services/desktop-bridge.service'; @@ -29,6 +35,15 @@ interface TrackedPointSeries extends TrackPromptPointMetadata { type TrackingOverlayStyle = 'point' | 'short' | 'full'; type DebugMaskSource = 'live' | 'manifest' | 'none'; +type ToastSeverity = 'error' | 'warning' | 'info' | 'success'; + +interface AppToast { + id: number; + severity: ToastSeverity; + title: string; + message: string; + createdAt: number; +} @Component({ selector: 'app-video-masker', @@ -37,7 +52,7 @@ type DebugMaskSource = 'live' | 'manifest' | 'none'; templateUrl: './video-masker.component.html', styleUrls: ['./video-masker.component.css'] }) -export class VideoMaskerComponent { +export class VideoMaskerComponent implements OnDestroy { @ViewChild('canvas') canvasRef!: ElementRef; @ViewChild('videoFileInput') videoFileInputRef?: ElementRef; @ViewChild('framesDirInput') framesDirInputRef?: ElementRef; @@ -67,6 +82,10 @@ export class VideoMaskerComponent { isLoading = signal(false); isFrameLoading = signal(false); isPointRequestInFlight = signal(false); + apiHealthStatus = signal('checking'); + activeJob = signal(null); + activeJobTitle = signal(''); + toasts = signal([]); lastClickRequestFrameIdx = signal(null); lastBackendResponseFrameIdx = signal(null); lastBackendResponseFrameFile = signal('n/a'); @@ -80,6 +99,8 @@ export class VideoMaskerComponent { private frameLoadToken = 0; private currentBaseImage: HTMLImageElement | null = null; private currentMaskObjects: { [objId: string]: VideoMaskObjectData } = {}; + private healthTimerId: ReturnType | null = null; + private nextToastId = 1; constructor( private backend: BackendService, @@ -100,6 +121,92 @@ export class VideoMaskerComponent { this.drawCurrentFrame(); } }); + + this.checkApiHealth(true); + this.healthTimerId = setInterval(() => this.checkApiHealth(), 3000); + } + + ngOnDestroy(): void { + if (this.healthTimerId !== null) { + clearInterval(this.healthTimerId); + } + } + + private async checkApiHealth(showChecking = false): Promise { + if (showChecking || this.apiHealthStatus() === 'checking') { + this.apiHealthStatus.set('checking'); + } + try { + await firstValueFrom(this.backend.health()); + this.apiHealthStatus.set('online'); + } catch { + this.apiHealthStatus.set('offline'); + } + } + + private showToast(severity: ToastSeverity, title: string, message: string): void { + const toast: AppToast = { + id: this.nextToastId++, + severity, + title, + message, + createdAt: Date.now(), + }; + this.toasts.update((existing) => [toast, ...existing].slice(0, 6)); + if (severity === 'info' || severity === 'success') { + setTimeout(() => this.dismissToast(toast.id), 5000); + } + } + + dismissToast(id: number): void { + this.toasts.update((existing) => existing.filter((toast) => toast.id !== id)); + } + + recentJobHistory(): JobStageHistoryEntry[] { + const history = this.activeJob()?.stage_history || []; + return history.slice(-3); + } + + private getErrorMessage(error: any, fallback: string): string { + return error?.error?.detail || error?.error?.error || error?.message || fallback; + } + + private delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + private async runBackendJob( + title: string, + startJob: () => Promise<{ job_id: string }>, + ): Promise { + this.isLoading.set(true); + this.activeJobTitle.set(title); + this.activeJob.set(null); + try { + const started = await startJob(); + while (true) { + const response = await firstValueFrom(this.backend.getJob(started.job_id)); + this.activeJob.set(response.job); + if (response.job.status === 'completed') { + return response.job.result as T; + } + if (response.job.status === 'failed') { + const message = response.job.error?.message || response.job.message || `${title} failed`; + this.showToast('error', title, message); + return null; + } + await this.delay(500); + } + } catch (error: any) { + console.error(error); + this.apiHealthStatus.set('offline'); + this.showToast('error', title, this.getErrorMessage(error, `${title} failed`)); + return null; + } finally { + this.activeJob.set(null); + this.activeJobTitle.set(''); + this.isLoading.set(false); + } } private updateStateEpoch(nextEpoch: number | undefined, source: string): void { @@ -194,10 +301,12 @@ export class VideoMaskerComponent { applyApiUrl() { this.apiUrlInput.set(this.backend.setApiUrl(this.apiUrlInput())); + this.checkApiHealth(true); } resetApiUrl() { this.apiUrlInput.set(this.backend.resetApiUrl()); + this.checkApiHealth(true); } async browseVideo() { @@ -275,11 +384,11 @@ export class VideoMaskerComponent { private showPathUnavailableMessage(target: 'video' | 'directory') { if (target === 'video') { - alert('Selected video file name is available, but this browser does not expose the full local path. Please paste the full video path manually.'); + this.showToast('warning', 'Path unavailable', 'Selected video file name is available, but this browser does not expose the full local path. Paste the full video path manually.'); return; } - alert('Selected folder contents are available, but this browser does not expose the full local directory path. Please paste the full frames directory path manually.'); + this.showToast('warning', 'Path unavailable', 'Selected folder contents are available, but this browser does not expose the full local directory path. Paste the full frames directory path manually.'); } isApiUrlDirty(): boolean { @@ -289,34 +398,31 @@ export class VideoMaskerComponent { async initVideo() { const enteredPath = this.videoDir().trim().replace(/^['\"]|['\"]$/g, ''); if (!enteredPath) { - alert('Please enter a valid video frames directory path or pick a video file.'); + this.showToast('warning', 'Missing video path', 'Enter a video frames directory path or pick a video file.'); return; } this.videoDir.set(enteredPath); - this.isLoading.set(true); - try { - const res = await firstValueFrom(this.backend.initVideoState(enteredPath)); - this.numFrames.set(res.num_frames); - this.targetFrameIdx.set(0); - this.displayedFrameIdx.set(-1); - this.hasManifestMasks.set(false); - this.trackedPoints.set([]); - this.masks.set(new Map()); - this.points.set(new Map()); - this.liveEditedObjectFrames.set(new Map()); - this.objects.set([{ id: 1, name: 'Object 1', color: this.getRandomColor() }]); - this.selectedObjectId.set(1); - this.updateStateEpoch(res.state_epoch, 'video init'); - this.resetDebugState(); - this.isInitialized.set(true); - } catch (err: any) { - console.error(err); - const errorMessage = err?.error?.detail || err?.error?.error || 'Failed to initialize video'; - alert(errorMessage); - } finally { - this.isLoading.set(false); + const res = await this.runBackendJob( + 'Loading video', + () => firstValueFrom(this.backend.initVideoState(enteredPath)), + ); + if (!res) { + return; } + this.numFrames.set(res.num_frames); + this.targetFrameIdx.set(0); + this.displayedFrameIdx.set(-1); + this.hasManifestMasks.set(false); + this.trackedPoints.set([]); + this.masks.set(new Map()); + this.points.set(new Map()); + this.liveEditedObjectFrames.set(new Map()); + this.objects.set([{ id: 1, name: 'Object 1', color: this.getRandomColor() }]); + this.selectedObjectId.set(1); + this.updateStateEpoch(res.state_epoch, 'video init'); + this.resetDebugState(); + this.isInitialized.set(true); } loadFrame(frameIdx: number) { @@ -803,46 +909,41 @@ export class VideoMaskerComponent { } async propagate() { - this.isLoading.set(true); - try { - const response = await firstValueFrom(this.backend.propagateInVideo({ + const response = await this.runBackendJob( + 'Propagating masks', + () => firstValueFrom(this.backend.propagateInVideo({ include_masks_in_response: false, include_saved_mask_paths: false - })); - this.updateStateEpoch(response.state_epoch, 'propagation'); - this.hasManifestMasks.set(Boolean(response.mask_manifest_path)); - this.loadFrame(this.targetFrameIdx()); - } catch (error) { - console.error(error); - alert('Propagation failed'); - } finally { - this.isLoading.set(false); + })), + ); + if (!response) { + return; } + this.updateStateEpoch(response.state_epoch, 'propagation'); + this.hasManifestMasks.set(Boolean(response.mask_manifest_path)); + this.loadFrame(this.targetFrameIdx()); } async runTracking() { - this.isLoading.set(true); - try { - const response = await firstValueFrom(this.backend.trackPromptPoints({ + const response = await this.runBackendJob( + 'Tracking prompt points', + () => firstValueFrom(this.backend.trackPromptPoints({ model_name: this.trackingModel(), add_support_grid: this.trackingUseSupportGrid() - })); - this.updateStateEpoch(response.state_epoch, 'tracking restore'); - - const trackedSeries: TrackedPointSeries[] = response.points.map((point, index) => ({ - ...point, - tracks: response.tracks[index] || [], - visibility: response.visibility[index] || [] - })); - this.trackedPoints.set(trackedSeries); - this.drawCurrentFrame(); - } catch (error: any) { - console.error(error); - const errorMessage = error?.error?.detail || 'Tracking failed'; - alert(errorMessage); - } finally { - this.isLoading.set(false); + })), + ); + if (!response) { + return; } + this.updateStateEpoch(response.state_epoch, 'tracking restore'); + + const trackedSeries: TrackedPointSeries[] = response.points.map((point, index) => ({ + ...point, + tracks: response.tracks[index] || [], + visibility: response.visibility[index] || [] + })); + this.trackedPoints.set(trackedSeries); + this.drawCurrentFrame(); } clearMasks() { @@ -858,7 +959,7 @@ export class VideoMaskerComponent { } save() { - alert('Save functionality not implemented yet.'); + this.showToast('info', 'Save not available', 'Save functionality is not implemented yet.'); } getRandomColor() { From 2735c5f9e19e51a4d5247c4324e2801940eeb637 Mon Sep 17 00:00:00 2001 From: "Rafael A." <157764758+HarenDev@users.noreply.github.com> Date: Tue, 28 Apr 2026 21:04:27 -0400 Subject: [PATCH 17/30] More frontend qol --- .gitignore | 5 +- backend/api.py | 483 +++++++++++++++--- backend/sam2_video_masker.py | 27 +- backend/utils.py | 4 +- frontend-ng/data-engine/angular.json | 3 +- .../src/app/services/backend.service.spec.ts | 13 + .../src/app/services/backend.service.ts | 16 + .../video-masker/video-masker.component.css | 12 + .../video-masker/video-masker.component.html | 13 +- .../video-masker.component.spec.ts | 20 + .../video-masker/video-masker.component.ts | 142 ++++- 11 files changed, 632 insertions(+), 106 deletions(-) diff --git a/.gitignore b/.gitignore index 8484239..8f693f6 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,7 @@ bedroom/ build/ apple_* *.log -/backend/.data_engine_frames/* \ No newline at end of file +/backend/.data_engine_frames/* +/backend/.data_engine_windows/* +/backend/cache/* +/backend/saved/* \ No newline at end of file diff --git a/backend/api.py b/backend/api.py index 41a8b83..ea9eabf 100644 --- a/backend/api.py +++ b/backend/api.py @@ -19,6 +19,7 @@ import shutil import uuid import threading +import re app = FastAPI() @@ -43,11 +44,18 @@ video_prompt_events: list[dict[str, Any]] = [] mask_manifest_path: Optional[str] = None video_state_epoch: int = 0 +active_session_dir: Optional[Path] = None +active_session_id: Optional[str] = None +active_session_saved_name: Optional[str] = None PROJECT_ROOT = Path(__file__).resolve().parent.parent +BACKEND_ROOT = PROJECT_ROOT / "backend" IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".bmp"} VIDEO_EXTENSIONS = {".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v"} -GENERATED_FRAMES_ROOT = PROJECT_ROOT / "backend/.data_engine_frames" -WINDOW_FRAMES_ROOT = PROJECT_ROOT / "backend/.data_engine_windows" +CACHE_ROOT = BACKEND_ROOT / "cache" +GENERATED_FRAMES_ROOT = CACHE_ROOT / "frames" +WINDOW_FRAMES_ROOT = CACHE_ROOT / "windows" +SESSION_CACHE_ROOT = CACHE_ROOT / "sessions" +SAVED_ROOT = BACKEND_ROOT / "saved" DEFAULT_MAX_MASK_FRAMES_IN_RESPONSE = int(os.getenv("VIDEO_PROPAGATE_MAX_MASK_FRAMES", "0")) DEFAULT_MAX_MASK_VALUES_IN_RESPONSE = int(os.getenv("VIDEO_PROPAGATE_MAX_MASK_VALUES", "0")) DEFAULT_PROMPT_TRACK_BATCH_SIZE = int(os.getenv("TRACK_PROMPT_BATCH_SIZE", "32")) @@ -251,6 +259,113 @@ def _cleanup_cuda_memory(): torch.cuda.empty_cache() +def _path_is_relative_to(path: Path, parent: Path) -> bool: + try: + path.resolve().relative_to(parent.resolve()) + return True + except ValueError: + return False + + +def _current_session_path() -> Optional[Path]: + return active_session_dir.resolve() if active_session_dir is not None else None + + +def _current_frames_dir() -> Optional[Path]: + session_path = _current_session_path() + return session_path / "frames" if session_path is not None else None + + +def _current_masks_dir() -> Optional[Path]: + session_path = _current_session_path() + return session_path / "masks" if session_path is not None else None + + +def _write_session_metadata(extra: Optional[dict[str, Any]] = None) -> None: + session_path = _current_session_path() + if session_path is None: + return + metadata = { + "session_id": active_session_id, + "saved_name": active_session_saved_name, + "source_video_path": video_source_path, + "resolved_video_frames_dir": video_dir, + "mask_manifest_path": mask_manifest_path, + "num_frames": len(video_frame_files), + "state_epoch": int(video_state_epoch), + "updated_at": _utc_now_iso(), + } + if video_masker is not None: + metadata.update( + { + "online_mode": video_masker.online_mode, + "batch_size": video_masker.default_batch_size, + "offload_video_to_cpu": video_masker.offload_video_to_cpu, + "offload_state_to_cpu": video_masker.offload_state_to_cpu, + } + ) + if extra: + metadata.update(extra) + write_mask_manifest(session_path / "session.json", metadata) + + +def _clear_active_cache_session() -> None: + session_path = _current_session_path() + if session_path is None: + return + if _path_is_relative_to(session_path, SESSION_CACHE_ROOT): + shutil.rmtree(session_path, ignore_errors=True) + + +def _clear_window_cache() -> None: + shutil.rmtree(WINDOW_FRAMES_ROOT, ignore_errors=True) + WINDOW_FRAMES_ROOT.mkdir(parents=True, exist_ok=True) + + +def _release_active_session( + *, + clear_video_masker: bool = True, + clear_tracker: bool = True, + clear_tracking_video: bool = True, + clear_video_state: bool = True, + clear_prompts: bool = True, + clear_cache_session: bool = False, +) -> None: + global video_masker, tracker, tracking_video, tracking_video_path + global video_dir, video_frame_files, video_source_path, video_prompt_events + global mask_manifest_path, active_session_dir, active_session_id, active_session_saved_name + + if clear_video_masker and video_masker is not None: + del video_masker + video_masker = None + if clear_tracker and tracker is not None: + del tracker + tracker = None + if clear_tracking_video: + tracking_video = None + tracking_video_path = None + if clear_cache_session: + _clear_active_cache_session() + if clear_video_state: + video_dir = None + video_frame_files = [] + video_source_path = None + mask_manifest_path = None + active_session_dir = None + active_session_id = None + active_session_saved_name = None + if clear_prompts: + video_prompt_events = [] + _cleanup_cuda_memory() + + +@app.on_event("shutdown") +def _cleanup_backend_cache_on_shutdown() -> None: + _release_active_session(clear_cache_session=False) + shutil.rmtree(CACHE_ROOT, ignore_errors=True) + _cleanup_cuda_memory() + + def _bump_video_state_epoch() -> int: global video_state_epoch video_state_epoch += 1 @@ -261,7 +376,6 @@ def _reset_video_session_state(): global video_prompt_events, mask_manifest_path, video_source_path video_prompt_events = [] mask_manifest_path = None - video_source_path = None def _record_prompt_event(request: "VideoAddPointsOrBoxRequest"): @@ -325,6 +439,102 @@ def _build_window_dir( return window_dir +def _link_or_copy_file(source_path: Path, target_path: Path) -> None: + target_path.parent.mkdir(parents=True, exist_ok=True) + try: + os.symlink(source_path, target_path) + except OSError: + try: + os.link(source_path, target_path) + except OSError: + shutil.copy2(source_path, target_path) + + +def _create_active_session(source_path: Path) -> Path: + global active_session_dir, active_session_id, active_session_saved_name + + SESSION_CACHE_ROOT.mkdir(parents=True, exist_ok=True) + session_id = uuid.uuid4().hex + session_dir = SESSION_CACHE_ROOT / session_id + (session_dir / "frames").mkdir(parents=True, exist_ok=False) + (session_dir / "masks").mkdir(parents=True, exist_ok=True) + active_session_dir = session_dir + active_session_id = session_id + active_session_saved_name = None + _write_session_metadata( + { + "created_at": _utc_now_iso(), + "source_input_path": str(source_path), + } + ) + return session_dir + + +def _copy_frames_directory_to_session( + source_dir: Path, + frames_dir: Path, + progress_callback: Optional[Callable[[int, int, Path], None]] = None, +) -> list[str]: + source_frames = sorted( + frame_path + for frame_path in source_dir.iterdir() + if frame_path.is_file() and frame_path.suffix.lower() in IMAGE_EXTENSIONS + ) + total = len(source_frames) + frame_names: list[str] = [] + for index, source_path in enumerate(source_frames, start=1): + target_name = f"{index - 1:05d}{source_path.suffix.lower()}" + target_path = frames_dir / target_name + _link_or_copy_file(source_path, target_path) + frame_names.append(target_name) + if progress_callback is not None: + progress_callback(index, total, source_path) + return frame_names + + +def _extract_video_to_session_frames( + video_path: Path, + frames_dir: Path, + progress_callback: Optional[Callable[[int, Optional[int]], None]] = None, +) -> list[str]: + frames_dir.mkdir(parents=True, exist_ok=True) + capture = cv2.VideoCapture(str(video_path)) + if not capture.isOpened(): + raise ValueError(f"Unable to open video file: {video_path}") + + raw_total_frames = int(capture.get(cv2.CAP_PROP_FRAME_COUNT) or 0) + total_frames: Optional[int] = raw_total_frames if raw_total_frames > 0 else None + frame_idx = 0 + frame_names: list[str] = [] + try: + while True: + success, frame = capture.read() + if not success: + break + frame_name = f"{frame_idx:05d}.jpg" + output_path = frames_dir / frame_name + if not cv2.imwrite(str(output_path), frame): + raise RuntimeError(f"Failed to write extracted frame: {output_path}") + frame_names.append(frame_name) + frame_idx += 1 + if progress_callback is not None: + progress_callback(frame_idx, total_frames) + finally: + capture.release() + + if frame_idx == 0: + raise ValueError(f"No frames could be extracted from video: {video_path}") + return frame_names + + +def _sanitize_save_name(name: str) -> str: + sanitized = re.sub(r'[<>:"/\\|?*\x00-\x1f]+', "_", name.strip()) + sanitized = re.sub(r"_+", "_", sanitized).strip(" ._") + if not sanitized or sanitized in {".", ".."}: + raise HTTPException(status_code=400, detail="Save name cannot be empty.") + return sanitized + + def _manifest_frame_payload(frame_masks: dict[int, np.ndarray]) -> dict[str, Any]: objects: dict[str, Any] = {} for obj_id, mask in frame_masks.items(): @@ -373,8 +583,11 @@ def _restore_video_masker_from_prompt_events( if video_dir is None: return + def _on_progress(stage: str, label: str, progress: Optional[float], message: str) -> None: + _update_job(stage=stage, stage_label=label, progress=progress, message=message) + if video_masker is None: - video_masker = svm.SAM2VideoMasker() + video_masker = svm.SAM2VideoMasker(progress_callback=_on_progress) video_masker.init_state( video_dir, @@ -383,6 +596,7 @@ def _restore_video_masker_from_prompt_events( offload_video_to_cpu=offload_video_to_cpu, offload_state_to_cpu=offload_state_to_cpu, async_loading_frames=False, + progress_callback=_on_progress, ) for event in video_prompt_events: @@ -399,6 +613,7 @@ def _restore_video_masker_from_prompt_events( if increment_epoch: _bump_video_state_epoch() + _write_session_metadata() def _load_tracking_video_from_current_video_state() -> tuple[np.ndarray, str]: @@ -466,20 +681,27 @@ def _resolve_input_path(path_value: str, expect_dir: Optional[bool] = None) -> P return resolved_path -def _prepare_video_masker_for_video_init(): - global video_masker, tracker, tracking_video, tracking_video_path, video_dir - - if tracker is not None: - del tracker - tracker = None - tracking_video = None - tracking_video_path = None - _cleanup_cuda_memory() +def _validate_video_input_path(resolved_input_path: Path) -> None: + if not resolved_input_path.is_file(): + return + suffix = resolved_input_path.suffix.lower() + if suffix in VIDEO_EXTENSIONS: + return + if suffix in IMAGE_EXTENSIONS: + detail = ( + f"Expected a frames directory or video file, got a single image file: {resolved_input_path}. " + "Provide a directory containing image frames." + ) + else: + detail = ( + f"Unsupported input file type: {resolved_input_path.suffix or ''}. " + "Provide a directory of image frames or a video file (.mp4, .mov, .avi, .mkv, .webm, .m4v)." + ) + raise HTTPException(status_code=400, detail=detail) - if video_masker is None: - video_masker = svm.SAM2VideoMasker() - _reset_video_session_state() +def _prepare_video_masker_for_video_init(): + _release_active_session(clear_cache_session=True) def _initialize_video_state_from_resolved_input( @@ -494,15 +716,15 @@ def _initialize_video_state_from_resolved_input( global video_masker, video_dir, video_frame_files, video_source_path source_video_path = None - extraction_reported = False + session_dir = _create_active_session(resolved_input_path) + frames_dir = session_dir / "frames" + if resolved_input_path.is_file(): suffix = resolved_input_path.suffix.lower() if suffix in VIDEO_EXTENSIONS: def _on_extract_progress(current: int, total: Optional[int]) -> None: - nonlocal extraction_reported - extraction_reported = True if total: - progress = min(0.7, 0.15 + (0.5 * (current / total))) + progress = 0.35 + (0.3 * (current / total)) message = f"Extracted {current} of {total} frames" else: progress = None @@ -519,10 +741,9 @@ def _on_extract_progress(current: int, total: Optional[int]) -> None: ) try: - resolved_video_dir = extract_video_to_frames( + indexed_frame_files = _extract_video_to_session_frames( resolved_input_path, - output_root=GENERATED_FRAMES_ROOT, - image_extensions=IMAGE_EXTENSIONS, + frames_dir, progress_callback=_on_extract_progress, ) except ValueError as error: @@ -530,16 +751,6 @@ def _on_extract_progress(current: int, total: Optional[int]) -> None: except Exception as error: raise HTTPException(status_code=500, detail=str(error)) from error source_video_path = str(resolved_input_path) - if not extraction_reported: - _update_job( - stage="indexing_frames", - stage_label="Indexing video frames", - progress=0.7, - current=None, - total=None, - frame_idx=None, - message="Found cached frame directory", - ) else: if suffix in IMAGE_EXTENSIONS: detail = ( @@ -553,18 +764,63 @@ def _on_extract_progress(current: int, total: Optional[int]) -> None: ) raise HTTPException(status_code=400, detail=detail) else: - resolved_video_dir = resolved_input_path + candidate_count = len([ + path + for path in resolved_input_path.iterdir() + if path.is_file() and path.suffix.lower() in IMAGE_EXTENSIONS + ]) + + def _on_link_progress(current: int, total: int, source_path: Path) -> None: + progress = 0.35 + (0.3 * (current / total)) if total else 0.65 + _update_job( + stage="linking_frames", + stage_label="Linking frame cache", + progress=progress, + current=current, + total=total, + frame_idx=max(0, current - 1), + message=f"Linked {current} of {total} frames", + append_history=current == 1 or current == total, + ) + + _update_job( + stage="linking_frames", + stage_label="Linking frame cache", + progress=0.35, + current=0, + total=candidate_count, + frame_idx=None, + message="Preparing session-local frame cache", + ) + indexed_frame_files = _copy_frames_directory_to_session( + resolved_input_path, + frames_dir, + progress_callback=_on_link_progress, + ) + + video_dir = str(frames_dir) + video_frame_files = indexed_frame_files + + if not video_frame_files: + raise HTTPException( + status_code=400, + detail=f"No image frames found in directory: {resolved_input_path}" + ) + + def _on_sam2_progress(stage: str, label: str, progress: Optional[float], message: str) -> None: + _update_job( + stage=stage, + stage_label=label, + progress=progress, + current=None, + total=None, + frame_idx=None, + message=message, + ) + + if video_masker is None: + video_masker = svm.SAM2VideoMasker(progress_callback=_on_sam2_progress) - video_dir = str(resolved_video_dir) - _update_job( - stage="initializing_state", - stage_label="Initializing video state", - progress=0.75, - current=None, - total=None, - frame_idx=None, - message="Initializing SAM2 state", - ) try: video_masker.init_state( video_dir, @@ -573,6 +829,7 @@ def _on_extract_progress(current: int, total: Optional[int]) -> None: offload_video_to_cpu=offload_video_to_cpu, offload_state_to_cpu=offload_state_to_cpu, async_loading_frames=async_loading_frames, + progress_callback=_on_sam2_progress, ) except ValueError as error: raise HTTPException(status_code=400, detail=str(error)) from error @@ -586,7 +843,7 @@ def _on_extract_progress(current: int, total: Optional[int]) -> None: candidate_paths = sorted([ frame_path - for frame_path in resolved_video_dir.iterdir() + for frame_path in frames_dir.iterdir() if frame_path.is_file() ]) total_candidates = len(candidate_paths) @@ -611,17 +868,14 @@ def _on_extract_progress(current: int, total: Optional[int]) -> None: message=f"Indexed {len(indexed_frame_files)} of {total_candidates} frame files", append_history=candidate_idx == 1 or candidate_idx == total_candidates, ) - - video_frame_files = indexed_frame_files - - if not video_frame_files: - raise HTTPException( - status_code=400, - detail=f"No image frames found in directory: {resolved_video_dir}" - ) - video_source_path = source_video_path state_epoch = _bump_video_state_epoch() + _write_session_metadata( + { + "created_at": _utc_now_iso(), + "source_input_path": str(resolved_input_path), + } + ) return { "message": "Video state initialized successfully", @@ -706,6 +960,10 @@ class VideoAddMaskRequest(BaseModel): mask: list[list[bool]] # 2D boolean mask +class VideoSaveRequest(BaseModel): + name: str + + class TrackingLoadVideoRequest(BaseModel): video_path: str model_name: str = "cotracker3_offline" # "cotracker3_offline" or "cotracker3_online" @@ -774,26 +1032,39 @@ async def init_video_state(request: VideoInitStateRequest): def _run_video_init_job(request: VideoInitStateRequest) -> dict[str, Any]: + global video_masker _update_job( status="running", stage="resolving_input", stage_label="Resolving input", - progress=0.1, + progress=0.05, message="Resolving video path", ) _prepare_video_masker_for_video_init() resolved_input_path = _resolve_input_path(request.video_frames_dir) + _validate_video_input_path(resolved_input_path) _update_job( - stage="loading_sam2", - stage_label="Loading SAM2", + stage="preparing_session_cache", + stage_label="Preparing session cache", + progress=0.10, + message="Creating active session cache", + ) + + def _on_sam2_progress(stage: str, label: str, progress: Optional[float], message: str) -> None: + _update_job(stage=stage, stage_label=label, progress=progress, message=message) + + video_masker = svm.SAM2VideoMasker(progress_callback=_on_sam2_progress) + _update_job( + stage="model_ready", + stage_label="SAM2 model ready", progress=0.35, - message="Preparing video masker", + message="SAM2 model loaded", ) if resolved_input_path.is_file() and resolved_input_path.suffix.lower() in VIDEO_EXTENSIONS: _update_job( stage="extracting_frames", - stage_label="Extracting frames", - progress=0.45, + stage_label="Extracting video frames", + progress=0.35, message="Extracting video frames", ) result = _initialize_video_state_from_resolved_input( @@ -816,14 +1087,21 @@ def _run_video_init_job(request: VideoInitStateRequest) -> dict[str, Any]: @app.post("/video/reset_state") async def reset_video_state(): - global video_masker + global video_masker, mask_manifest_path if video_masker is None: return {"error": "Video masker not active."} video_masker.reset_state() _reset_video_session_state() + masks_dir = _current_masks_dir() + if masks_dir is not None: + shutil.rmtree(masks_dir, ignore_errors=True) + masks_dir.mkdir(parents=True, exist_ok=True) + mask_manifest_path = None + state_epoch = _bump_video_state_epoch() + _write_session_metadata() return { "message": "Video state reset successfully", - "state_epoch": _bump_video_state_epoch(), + "state_epoch": state_epoch, } @app.post("/video/add_new_points_or_box") @@ -957,6 +1235,48 @@ async def add_new_mask(request: VideoAddMaskRequest): "out_masks": masks_list } + +@app.post("/video/save") +async def save_video_session(request: VideoSaveRequest): + global active_session_dir, active_session_saved_name, video_dir, mask_manifest_path + + session_path = _current_session_path() + if session_path is None or video_dir is None or not video_frame_files: + raise HTTPException(status_code=400, detail="Video session is not initialized.") + + save_name = _sanitize_save_name(request.name) + SAVED_ROOT.mkdir(parents=True, exist_ok=True) + saved_path = (SAVED_ROOT / save_name).resolve() + if saved_path.exists(): + raise HTTPException(status_code=409, detail=f"Saved session already exists: {save_name}") + + if _path_is_relative_to(session_path, SAVED_ROOT): + raise HTTPException(status_code=409, detail="Current session is already saved.") + + session_path.parent.mkdir(parents=True, exist_ok=True) + shutil.move(str(session_path), str(saved_path)) + + active_session_dir = saved_path + active_session_saved_name = save_name + video_dir = str(saved_path / "frames") + manifest_path = saved_path / "masks" / "manifest.json" + mask_manifest_path = str(manifest_path) if manifest_path.exists() else None + _write_session_metadata( + { + "saved_name": save_name, + "saved_path": str(saved_path), + "saved_at": _utc_now_iso(), + } + ) + + return { + "message": "Session saved successfully", + "name": save_name, + "saved_path": str(saved_path), + "state_epoch": int(video_state_epoch), + } + + @app.post("/video/propagate_in_video") async def propagate_in_video(request: VideoPropagateRequest): return _queue_long_job( @@ -1036,16 +1356,20 @@ def _propagation_progress(processed_frames_count: int, expected_frames_count: in raise HTTPException(status_code=400, detail="Invalid propagation frame range.") expected_total_frames = end_frame_idx - start_frame_idx + 1 + masks_root = _current_masks_dir() + if masks_root is None: + raise HTTPException(status_code=400, detail="Video session cache is not initialized.") + _clear_window_cache() _update_job( - stage="preparing_manifest", - stage_label="Preparing manifest", + stage="clearing_previous_masks", + stage_label="Clearing previous masks", progress=0.02, current=0, total=expected_total_frames, - message=f"Preparing masks for {expected_total_frames} frames", + message="Preparing mask output directory", ) - frame_files, masks_dir = prepare_video_masks_output(video_dir) + frame_files, masks_dir = prepare_video_masks_output(video_dir, masks_root) manifest_file_path = masks_dir / "manifest.json" first_frame = cv2.imread(str(Path(video_dir) / video_frame_files[start_frame_idx])) @@ -1294,6 +1618,7 @@ def _on_window_frame(local_frame_idx: int, frame_masks: dict[int, np.ndarray]): write_mask_manifest(manifest_file_path, manifest) mask_manifest_path = str(manifest_file_path) + _write_session_metadata() _update_job( stage="saving_manifest", stage_label="Saving manifest", @@ -1309,9 +1634,9 @@ def _on_window_frame(local_frame_idx: int, frame_masks: dict[int, np.ndarray]): try: # Rebind interactive state to the original full video frame index space. _update_job( - stage="restoring_state", + stage="restoring_interactive_state", stage_label="Restoring interactive state", - progress=0.99, + progress=None, current=len(processed_frames), total=expected_total_frames, window_index=None, @@ -1376,15 +1701,10 @@ async def remove_object(obj_id: int): @app.post("/tracking/load_video") async def load_tracking_video(request: TrackingLoadVideoRequest): """Load a video file for tracking.""" - global tracker, tracking_video, tracking_video_path, video_masker, video_dir - - # Unload video masker if it's currently loaded - if video_masker is not None: - del video_masker - video_masker = None - video_dir = None - _bump_video_state_epoch() - _cleanup_cuda_memory() + global tracker, tracking_video, tracking_video_path + + _release_active_session(clear_tracker=False, clear_cache_session=True) + _bump_video_state_epoch() # (Re-)initialise tracker with the requested model variant. # A new tracker is created if the model_name changed or no tracker exists. @@ -1802,7 +2122,8 @@ async def get_mask_manifest(): if video_dir is None: return {"error": "Video not initialized"} - manifest_path = Path(mask_manifest_path) if mask_manifest_path else Path(video_dir) / "masks" / "manifest.json" + masks_dir = _current_masks_dir() + manifest_path = Path(mask_manifest_path) if mask_manifest_path else (masks_dir / "manifest.json" if masks_dir is not None else Path(video_dir) / "masks" / "manifest.json") if not manifest_path.exists(): return {"error": "Mask manifest not found. Run /video/propagate_in_video first."} @@ -1826,7 +2147,8 @@ async def get_mask_data(frame_idx: int): if frame_idx < 0: return {"error": "Frame index out of bounds"} - manifest_path = Path(mask_manifest_path) if mask_manifest_path else Path(video_dir) / "masks" / "manifest.json" + masks_dir = _current_masks_dir() + manifest_path = Path(mask_manifest_path) if mask_manifest_path else (masks_dir / "manifest.json" if masks_dir is not None else Path(video_dir) / "masks" / "manifest.json") if not manifest_path.exists(): return {"frame_idx": frame_idx, "objects": {}} @@ -1866,7 +2188,8 @@ async def get_video_mask_frame(frame_idx: int): if frame_idx < 0: return {"error": "Frame index out of bounds"} - file_path = Path(video_dir) / "masks" / f"frame_{frame_idx:05d}_masks.png" + masks_dir = _current_masks_dir() or Path(video_dir) / "masks" + file_path = masks_dir / f"frame_{frame_idx:05d}_masks.png" if not file_path.exists(): raise HTTPException(status_code=404, detail=f"Mask frame not found: {file_path}") return FileResponse(str(file_path)) diff --git a/backend/sam2_video_masker.py b/backend/sam2_video_masker.py index 2dab4b0..a5bd7fa 100644 --- a/backend/sam2_video_masker.py +++ b/backend/sam2_video_masker.py @@ -3,6 +3,7 @@ import os import gc from pathlib import Path +from typing import Callable, Optional from sam2.sam2_video_predictor import SAM2VideoPredictor from utils import extract_video_to_frames @@ -13,11 +14,15 @@ IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".bmp"} VIDEO_EXTENSIONS = {".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v"} PROJECT_ROOT = Path(__file__).resolve().parent.parent -GENERATED_FRAMES_ROOT = PROJECT_ROOT / "backend/.data_engine_frames" +GENERATED_FRAMES_ROOT = PROJECT_ROOT / "backend/cache/frames" class SAM2VideoMasker: - def __init__(self): + def __init__(self, progress_callback: Optional[Callable[[str, str, Optional[float], str], None]] = None): + def _report(stage: str, label: str, progress: Optional[float], message: str) -> None: + if progress_callback is not None: + progress_callback(stage, label, progress, message) + if torch.cuda.is_available(): self.device = torch.device("cuda") elif torch.backends.mps.is_available(): @@ -63,7 +68,9 @@ def __init__(self): "give numerically different outputs and sometimes degraded performance on MPS." ) + _report("loading_sam2_model", "Loading SAM2 model", None, "Loading SAM2 model weights") self.predictor = SAM2VideoPredictor.from_pretrained("facebook/sam2-hiera-large") + _report("model_ready", "SAM2 model ready", 0.35, "SAM2 model loaded") self.inference_state = None self.online_mode = True @@ -79,6 +86,7 @@ def init_state( offload_video_to_cpu=None, offload_state_to_cpu=None, async_loading_frames=False, + progress_callback: Optional[Callable[[str, str, Optional[float], str], None]] = None, ): self.online_mode = bool(online_mode) @@ -124,6 +132,14 @@ def init_state( else: resolved_video_dir = resolved_input_path + if progress_callback is not None: + progress_callback( + "initializing_sam2_video_state", + "Initializing SAM2 video state", + None, + "Loading frames into SAM2 state", + ) + self.inference_state = self.predictor.init_state( video_path=str(resolved_video_dir), offload_video_to_cpu=self.offload_video_to_cpu, @@ -131,6 +147,13 @@ def init_state( async_loading_frames=async_loading_frames, ) self.predictor.reset_state(self.inference_state) + if progress_callback is not None: + progress_callback( + "indexing_frames", + "Indexing video frames", + 0.85, + "SAM2 state initialized; indexing frame files", + ) def reset_state(self): self.predictor.reset_state(self.inference_state) diff --git a/backend/utils.py b/backend/utils.py index 2e71143..e02f32e 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -194,7 +194,7 @@ def save_video_masks(video_dir, video_segments): return saved_paths -def prepare_video_masks_output(video_dir): +def prepare_video_masks_output(video_dir, masks_dir=None): """ Prepare output directory and frame file list for streaming mask writes. @@ -203,7 +203,7 @@ def prepare_video_masks_output(video_dir): - masks_dir: output directory path """ video_path = Path(video_dir) - masks_dir = video_path / "masks" + masks_dir = Path(masks_dir) if masks_dir is not None else video_path / "masks" if masks_dir.exists(): shutil.rmtree(masks_dir) diff --git a/frontend-ng/data-engine/angular.json b/frontend-ng/data-engine/angular.json index ca58ffa..e2c2b1a 100644 --- a/frontend-ng/data-engine/angular.json +++ b/frontend-ng/data-engine/angular.json @@ -2,7 +2,8 @@ "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { - "packageManager": "npm" + "packageManager": "npm", + "analytics": "803b5ab3-15ec-45f2-a440-e1bab219526d" }, "newProjectRoot": "projects", "projects": { diff --git a/frontend-ng/data-engine/src/app/services/backend.service.spec.ts b/frontend-ng/data-engine/src/app/services/backend.service.spec.ts index 5a6dbf2..268c66b 100644 --- a/frontend-ng/data-engine/src/app/services/backend.service.spec.ts +++ b/frontend-ng/data-engine/src/app/services/backend.service.spec.ts @@ -87,4 +87,17 @@ describe('BackendService', () => { expect((http.get as any).mock.calls[0][0]).toBe('http://127.0.0.1:8000/jobs/abc123'); }); + + it('saves a video session by name', () => { + const http = { + post: vi.fn(() => of({})), + get: vi.fn(), + } as unknown as HttpClient; + const service = new BackendService(http); + + service.saveVideoSession('review-run').subscribe(); + + expect((http.post as any).mock.calls[0][0]).toBe('http://127.0.0.1:8000/video/save'); + expect((http.post as any).mock.calls[0][1]).toEqual({ name: 'review-run' }); + }); }); diff --git a/frontend-ng/data-engine/src/app/services/backend.service.ts b/frontend-ng/data-engine/src/app/services/backend.service.ts index 27702fc..181d2df 100644 --- a/frontend-ng/data-engine/src/app/services/backend.service.ts +++ b/frontend-ng/data-engine/src/app/services/backend.service.ts @@ -41,6 +41,17 @@ export interface VideoAddMaskRequest { mask: boolean[][]; } +export interface VideoSaveRequest { + name: string; +} + +export interface VideoSaveResponse { + message: string; + name: string; + saved_path: string; + state_epoch: number; +} + export interface VideoAddPointsResponse { request_frame_idx: number; frame_idx: number; @@ -299,6 +310,11 @@ export class BackendService { return this.http.post(this.endpoint('/video/propagate_in_video'), request); } + saveVideoSession(name: string): Observable { + const payload: VideoSaveRequest = { name }; + return this.http.post(this.endpoint('/video/save'), payload); + } + clearAllPromptsInFrame(frameIdx: number, objId: number): Observable { return this.http.post(this.endpoint('/video/clear_all_prompts_in_frame'), null, { params: { frame_idx: frameIdx.toString(), obj_id: objId.toString() } diff --git a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.css b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.css index 8ed6cee..c19d7e6 100644 --- a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.css +++ b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.css @@ -160,6 +160,18 @@ canvas { padding: 4px 6px; } +.save-controls { + display: grid; + grid-template-columns: 1fr auto; + gap: 6px; + margin-top: 8px; +} + +.save-controls input { + min-width: 0; + padding: 4px 6px; +} + .debug-section { font-size: 0.85rem; } diff --git a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.html b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.html index 5238d00..db843ea 100644 --- a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.html +++ b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.html @@ -107,7 +107,16 @@

Actions

- +
+ + +
@@ -135,7 +144,7 @@

Sync Debug

Target: {{ targetFrameIdx() }} | Displayed: {{ displayedFrameIdx() }} / {{ numFrames() - 1 }} + [disabled]="isLoading() || isPointRequestInFlight()">
diff --git a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.spec.ts b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.spec.ts index dd74f8f..0f5a691 100644 --- a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.spec.ts +++ b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.spec.ts @@ -15,6 +15,7 @@ describe('VideoMaskerComponent sync contract', () => { getApiUrl: ReturnType; setApiUrl: ReturnType; resetApiUrl: ReturnType; + saveVideoSession: ReturnType; }; let desktopBridgeMock: { isTauri: ReturnType; @@ -43,6 +44,7 @@ describe('VideoMaskerComponent sync contract', () => { getApiUrl: vi.fn(() => 'http://127.0.0.1:8000'), setApiUrl: vi.fn((value: string) => value), resetApiUrl: vi.fn(() => 'http://127.0.0.1:8000'), + saveVideoSession: vi.fn(), }; desktopBridgeMock = { isTauri: vi.fn(() => false), @@ -63,6 +65,7 @@ describe('VideoMaskerComponent sync contract', () => { getApiUrl: backendMock.getApiUrl, setApiUrl: backendMock.setApiUrl, resetApiUrl: backendMock.resetApiUrl, + saveVideoSession: backendMock.saveVideoSession, }, }, { @@ -241,4 +244,21 @@ describe('VideoMaskerComponent sync contract', () => { expect(component.toasts()[0].title).toBe('Loading video'); expect(component.toasts()[0].message).toBe('Path not found'); }); + + it('saves the current session with the typed name', async () => { + backendMock.saveVideoSession.mockReturnValue(of({ + message: 'Session saved successfully', + name: 'review-run', + saved_path: 'C:/project/backend/saved/review-run', + state_epoch: 3, + })); + component.isInitialized.set(true); + component.saveName.set('review-run'); + + component.save(); + await Promise.resolve(); + + expect(backendMock.saveVideoSession).toHaveBeenCalledWith('review-run'); + expect(component.toasts()[0].title).toBe('Session saved'); + }); }); diff --git a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.ts b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.ts index 2196f4f..bed83db 100644 --- a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.ts +++ b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.ts @@ -12,7 +12,8 @@ import { VideoAddPointsOrBoxRequest, VideoInitStateResponse, VideoMaskObjectData, - VideoPropagateResponse + VideoPropagateResponse, + VideoSaveResponse } from '../services/backend.service'; import { DesktopBridgeService } from '../services/desktop-bridge.service'; @@ -73,6 +74,7 @@ export class VideoMaskerComponent implements OnDestroy { points = signal>>(new Map()); liveEditedObjectFrames = signal>>(new Map()); hasManifestMasks = signal(false); + saveName = signal(''); trackingModel = signal<'cotracker3_online' | 'cotracker3_offline'>('cotracker3_online'); trackingOverlayStyle = signal('short'); @@ -97,6 +99,11 @@ export class VideoMaskerComponent implements OnDestroy { lastDiscardReason = signal(null); private frameLoadToken = 0; + private pendingFrameIdx: number | null = null; + private frameLoadAnimationId: number | null = null; + private frameImageCache = new Map(); + private maskDataCache = new Map(); + private readonly maxFrameCacheSize = 24; private currentBaseImage: HTMLImageElement | null = null; private currentMaskObjects: { [objId: string]: VideoMaskObjectData } = {}; private healthTimerId: ReturnType | null = null; @@ -110,7 +117,7 @@ export class VideoMaskerComponent implements OnDestroy { effect(() => { if (this.isInitialized()) { - this.loadFrame(this.targetFrameIdx()); + this.scheduleFrameLoad(this.targetFrameIdx()); } }); @@ -130,6 +137,9 @@ export class VideoMaskerComponent implements OnDestroy { if (this.healthTimerId !== null) { clearInterval(this.healthTimerId); } + if (this.frameLoadAnimationId !== null) { + cancelAnimationFrame(this.frameLoadAnimationId); + } } private async checkApiHealth(showChecking = false): Promise { @@ -299,6 +309,10 @@ export class VideoMaskerComponent implements OnDestroy { this.apiUrlInput.set(value); } + onSaveNameChange(value: string) { + this.saveName.set(value); + } + applyApiUrl() { this.apiUrlInput.set(this.backend.setApiUrl(this.apiUrlInput())); this.checkApiHealth(true); @@ -414,10 +428,12 @@ export class VideoMaskerComponent implements OnDestroy { this.targetFrameIdx.set(0); this.displayedFrameIdx.set(-1); this.hasManifestMasks.set(false); + this.saveName.set(''); this.trackedPoints.set([]); this.masks.set(new Map()); this.points.set(new Map()); this.liveEditedObjectFrames.set(new Map()); + this.clearFrameCaches(); this.objects.set([{ id: 1, name: 'Object 1', color: this.getRandomColor() }]); this.selectedObjectId.set(1); this.updateStateEpoch(res.state_epoch, 'video init'); @@ -425,16 +441,49 @@ export class VideoMaskerComponent implements OnDestroy { this.isInitialized.set(true); } + private clearFrameCaches(): void { + this.frameImageCache.clear(); + this.maskDataCache.clear(); + this.currentBaseImage = null; + this.currentMaskObjects = {}; + this.frameLoadToken++; + if (this.frameLoadAnimationId !== null) { + cancelAnimationFrame(this.frameLoadAnimationId); + this.frameLoadAnimationId = null; + } + } + + private scheduleFrameLoad(frameIdx: number) { + this.pendingFrameIdx = frameIdx; + if (this.frameLoadAnimationId !== null) { + return; + } + this.frameLoadAnimationId = requestAnimationFrame(() => { + this.frameLoadAnimationId = null; + const nextFrameIdx = this.pendingFrameIdx; + this.pendingFrameIdx = null; + if (nextFrameIdx !== null) { + this.loadFrame(nextFrameIdx); + } + }); + } + loadFrame(frameIdx: number) { if (!this.canvasRef?.nativeElement) { return; } const token = ++this.frameLoadToken; + this.currentMaskObjects = {}; + const cachedImage = this.frameImageCache.get(frameIdx); + if (cachedImage?.complete) { + this.paintLoadedFrame(cachedImage, frameIdx, token); + return; + } + this.isFrameLoading.set(true); const image = new Image(); const frameUrl = this.backend.getVideoFrameUrl(frameIdx); - this.currentMaskObjects = {}; image.onerror = () => { if (token !== this.frameLoadToken) { @@ -448,22 +497,52 @@ export class VideoMaskerComponent implements OnDestroy { if (token !== this.frameLoadToken) { return; } + this.cacheFrameImage(frameIdx, image); + this.paintLoadedFrame(image, frameIdx, token); + }; - this.currentBaseImage = image; - this.ensureCanvasSize(image.width, image.height); - this.draw(image, frameIdx); - this.displayedFrameIdx.set(frameIdx); - // The displayed frame is now stable on canvas, so allow interaction immediately. - this.isFrameLoading.set(false); + image.src = frameUrl; + } - await this.loadMaskDataForFrame(frameIdx, token); - if (token !== this.frameLoadToken) { - return; + private async paintLoadedFrame(image: HTMLImageElement, frameIdx: number, token: number) { + this.currentBaseImage = image; + this.ensureCanvasSize(image.width, image.height); + this.draw(image, frameIdx); + this.displayedFrameIdx.set(frameIdx); + this.isFrameLoading.set(false); + this.preloadNeighborFrames(frameIdx); + + await this.loadMaskDataForFrame(frameIdx, token); + if (token !== this.frameLoadToken) { + return; + } + this.draw(image, frameIdx); + } + + private cacheFrameImage(frameIdx: number, image: HTMLImageElement) { + if (this.frameImageCache.has(frameIdx)) { + this.frameImageCache.delete(frameIdx); + } + this.frameImageCache.set(frameIdx, image); + while (this.frameImageCache.size > this.maxFrameCacheSize) { + const oldestKey = this.frameImageCache.keys().next().value; + if (oldestKey === undefined) { + break; } - this.draw(image, frameIdx); - }; + this.frameImageCache.delete(oldestKey); + this.maskDataCache.delete(oldestKey); + } + } - image.src = frameUrl; + private preloadNeighborFrames(frameIdx: number) { + for (const neighborIdx of [frameIdx + 1, frameIdx - 1]) { + if (neighborIdx < 0 || neighborIdx >= this.numFrames() || this.frameImageCache.has(neighborIdx)) { + continue; + } + const image = new Image(); + image.onload = () => this.cacheFrameImage(neighborIdx, image); + image.src = this.backend.getVideoFrameUrl(neighborIdx); + } } private ensureCanvasSize(width: number, height: number) { @@ -482,6 +561,11 @@ export class VideoMaskerComponent implements OnDestroy { this.currentMaskObjects = {}; return; } + const cachedMaskData = this.maskDataCache.get(frameIdx); + if (cachedMaskData) { + this.currentMaskObjects = cachedMaskData; + return; + } try { const response = await firstValueFrom(this.backend.getVideoMaskData(frameIdx)); @@ -494,6 +578,7 @@ export class VideoMaskerComponent implements OnDestroy { return; } this.currentMaskObjects = response.objects || {}; + this.maskDataCache.set(frameIdx, this.currentMaskObjects); } catch (error) { console.error(error); this.currentMaskObjects = {}; @@ -921,7 +1006,8 @@ export class VideoMaskerComponent implements OnDestroy { } this.updateStateEpoch(response.state_epoch, 'propagation'); this.hasManifestMasks.set(Boolean(response.mask_manifest_path)); - this.loadFrame(this.targetFrameIdx()); + this.maskDataCache.clear(); + this.scheduleFrameLoad(this.targetFrameIdx()); } async runTracking() { @@ -954,12 +1040,32 @@ export class VideoMaskerComponent implements OnDestroy { this.masks.set(new Map()); this.points.set(new Map()); this.liveEditedObjectFrames.set(new Map()); - this.loadFrame(this.targetFrameIdx()); + this.maskDataCache.clear(); + this.scheduleFrameLoad(this.targetFrameIdx()); }); } save() { - this.showToast('info', 'Save not available', 'Save functionality is not implemented yet.'); + const name = this.saveName().trim(); + if (!name) { + this.showToast('warning', 'Missing save name', 'Enter a name for this saved session.'); + return; + } + + this.isLoading.set(true); + firstValueFrom(this.backend.saveVideoSession(name)) + .then((response: VideoSaveResponse) => { + this.updateStateEpoch(response.state_epoch, 'save'); + this.saveName.set(response.name); + this.showToast('success', 'Session saved', `Saved to ${response.saved_path}`); + }) + .catch((error: any) => { + console.error(error); + this.showToast('error', 'Save failed', this.getErrorMessage(error, 'Session save failed')); + }) + .finally(() => { + this.isLoading.set(false); + }); } getRandomColor() { From 31a8414f75f4e5901d1fac192eba73709c0dabbe Mon Sep 17 00:00:00 2001 From: "Rafael A." <157764758+HarenDev@users.noreply.github.com> Date: Wed, 29 Apr 2026 00:17:21 -0400 Subject: [PATCH 18/30] Questionable commit (saving progress here for today, this commit was meant to add proper save/load support) --- backend/api.py | 512 +++++++++++++++--- .../src/app/services/backend.service.spec.ts | 32 ++ .../src/app/services/backend.service.ts | 49 +- .../video-masker/video-masker.component.css | 157 +++++- .../video-masker/video-masker.component.html | 72 ++- .../video-masker.component.spec.ts | 186 ++++++- .../video-masker/video-masker.component.ts | 462 ++++++++++++++-- 7 files changed, 1296 insertions(+), 174 deletions(-) diff --git a/backend/api.py b/backend/api.py index ea9eabf..f74c16b 100644 --- a/backend/api.py +++ b/backend/api.py @@ -1,7 +1,7 @@ from typing import Optional, Any, Callable from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict, Field, ValidationError, field_validator, model_validator import numpy as np import gc import logging @@ -59,6 +59,7 @@ DEFAULT_MAX_MASK_FRAMES_IN_RESPONSE = int(os.getenv("VIDEO_PROPAGATE_MAX_MASK_FRAMES", "0")) DEFAULT_MAX_MASK_VALUES_IN_RESPONSE = int(os.getenv("VIDEO_PROPAGATE_MAX_MASK_VALUES", "0")) DEFAULT_PROMPT_TRACK_BATCH_SIZE = int(os.getenv("TRACK_PROMPT_BATCH_SIZE", "32")) +DEFAULT_MAX_INTERACTIVE_LIVE_MASKS = int(os.getenv("VIDEO_SAVE_MAX_LIVE_MASKS", "5000")) logger = logging.getLogger(__name__) current_job: Optional[dict[str, Any]] = None @@ -285,6 +286,15 @@ def _write_session_metadata(extra: Optional[dict[str, Any]] = None) -> None: session_path = _current_session_path() if session_path is None: return + existing_metadata: dict[str, Any] = {} + session_metadata_path = session_path / "session.json" + if session_metadata_path.exists(): + try: + loaded = load_mask_manifest(session_metadata_path) + if isinstance(loaded, dict): + existing_metadata = loaded + except Exception: + logger.warning("Failed to read existing session metadata from %s", session_metadata_path) metadata = { "session_id": active_session_id, "saved_name": active_session_saved_name, @@ -304,9 +314,19 @@ def _write_session_metadata(extra: Optional[dict[str, Any]] = None) -> None: "offload_state_to_cpu": video_masker.offload_state_to_cpu, } ) + for key in ( + "schema_version", + "interactive_state", + "created_at", + "saved_at", + "saved_path", + "source_input_path", + ): + if key in existing_metadata: + metadata[key] = existing_metadata[key] if extra: metadata.update(extra) - write_mask_manifest(session_path / "session.json", metadata) + write_mask_manifest(session_metadata_path, metadata) def _clear_active_cache_session() -> None: @@ -700,6 +720,49 @@ def _validate_video_input_path(resolved_input_path: Path) -> None: raise HTTPException(status_code=400, detail=detail) +def _resolve_saved_session_layout(resolved_input_path: Path) -> Optional[tuple[Path, Path, Path]]: + if not resolved_input_path.is_dir(): + return None + session_json = resolved_input_path / "session.json" + if not session_json.exists(): + return None + frames_dir = resolved_input_path / "frames" + masks_dir = resolved_input_path / "masks" + if not frames_dir.is_dir(): + raise HTTPException( + status_code=400, + detail=f"Saved session directory is missing required frames/ folder: {frames_dir}", + ) + if not masks_dir.is_dir(): + raise HTTPException( + status_code=400, + detail=f"Saved session directory is missing required masks/ folder: {masks_dir}", + ) + return resolved_input_path.resolve(), frames_dir.resolve(), masks_dir.resolve() + + +def _load_session_metadata(session_root: Path) -> dict[str, Any]: + session_json_path = session_root / "session.json" + try: + metadata = load_mask_manifest(session_json_path) + except FileNotFoundError as error: + raise HTTPException( + status_code=400, + detail=f"Saved session directory is missing session.json: {session_json_path}", + ) from error + except Exception as error: + raise HTTPException( + status_code=400, + detail=f"Unable to parse saved session metadata: {session_json_path}", + ) from error + if not isinstance(metadata, dict): + raise HTTPException( + status_code=400, + detail=f"Saved session metadata must be a JSON object: {session_json_path}", + ) + return metadata + + def _prepare_video_masker_for_video_init(): _release_active_session(clear_cache_session=True) @@ -714,99 +777,159 @@ def _initialize_video_state_from_resolved_input( async_loading_frames: bool, ): global video_masker, video_dir, video_frame_files, video_source_path + global active_session_dir, active_session_id, active_session_saved_name, mask_manifest_path source_video_path = None - session_dir = _create_active_session(resolved_input_path) - frames_dir = session_dir / "frames" + source_type = "frames_dir" + restored_session_payload: Optional[dict[str, Any]] = None + saved_session_layout = _resolve_saved_session_layout(resolved_input_path) + + if saved_session_layout is not None: + session_dir, frames_dir, masks_dir = saved_session_layout + session_metadata = _load_session_metadata(session_dir) + source_type = "saved_session" + active_session_dir = session_dir + active_session_id = str(session_metadata.get("session_id") or uuid.uuid4().hex) + active_session_saved_name = ( + str(session_metadata.get("saved_name")).strip() + if session_metadata.get("saved_name") + else session_dir.name + ) + manifest_path = masks_dir / "manifest.json" + mask_manifest_path = str(manifest_path) if manifest_path.exists() else None + source_video_path = session_metadata.get("source_video_path") + video_source_path = source_video_path + indexed_frame_files = sorted( + frame_path.name + for frame_path in frames_dir.iterdir() + if frame_path.is_file() and frame_path.suffix.lower() in IMAGE_EXTENSIONS + ) + _update_job( + stage="linking_frames", + stage_label="Loading saved session frames", + progress=0.65, + current=len(indexed_frame_files), + total=len(indexed_frame_files), + frame_idx=len(indexed_frame_files) - 1 if indexed_frame_files else None, + message=f"Found {len(indexed_frame_files)} saved session frames", + ) + interactive_state, restore_warnings = _validate_interactive_state_for_restore( + session_metadata.get("interactive_state") + ) + restored_session_payload = { + "session_meta": session_metadata, + "interactive_state": interactive_state, + "has_mask_manifest": bool(mask_manifest_path), + "interactive_state_warnings": restore_warnings, + } + else: + session_dir = _create_active_session(resolved_input_path) + frames_dir = session_dir / "frames" + + if resolved_input_path.is_file(): + source_type = "video_file" + suffix = resolved_input_path.suffix.lower() + if suffix in VIDEO_EXTENSIONS: + def _on_extract_progress(current: int, total: Optional[int]) -> None: + if total: + progress = 0.35 + (0.3 * (current / total)) + message = f"Extracted {current} of {total} frames" + else: + progress = None + message = f"Extracted {current} frames" + _update_job( + stage="extracting_frames", + stage_label="Extracting video frames", + progress=progress, + current=current, + total=total, + frame_idx=max(0, current - 1), + message=message, + append_history=current == 1 or (bool(total) and current == total), + ) - if resolved_input_path.is_file(): - suffix = resolved_input_path.suffix.lower() - if suffix in VIDEO_EXTENSIONS: - def _on_extract_progress(current: int, total: Optional[int]) -> None: - if total: - progress = 0.35 + (0.3 * (current / total)) - message = f"Extracted {current} of {total} frames" + try: + indexed_frame_files = _extract_video_to_session_frames( + resolved_input_path, + frames_dir, + progress_callback=_on_extract_progress, + ) + except ValueError as error: + raise HTTPException(status_code=400, detail=str(error)) from error + except Exception as error: + raise HTTPException(status_code=500, detail=str(error)) from error + source_video_path = str(resolved_input_path) + else: + if suffix in IMAGE_EXTENSIONS: + detail = ( + f"Expected a frames directory or video file, got a single image file: {resolved_input_path}. " + "Provide a directory containing image frames." + ) else: - progress = None - message = f"Extracted {current} frames" + detail = ( + f"Unsupported input file type: {resolved_input_path.suffix or ''}. " + "Provide a directory of image frames or a video file (.mp4, .mov, .avi, .mkv, .webm, .m4v)." + ) + raise HTTPException(status_code=400, detail=detail) + else: + source_type = "frames_dir" + candidate_count = len([ + path + for path in resolved_input_path.iterdir() + if path.is_file() and path.suffix.lower() in IMAGE_EXTENSIONS + ]) + + def _on_link_progress(current: int, total: int, source_path: Path) -> None: + progress = 0.35 + (0.3 * (current / total)) if total else 0.65 _update_job( - stage="extracting_frames", - stage_label="Extracting video frames", + stage="linking_frames", + stage_label="Linking frame cache", progress=progress, current=current, total=total, frame_idx=max(0, current - 1), - message=message, - append_history=current == 1 or (bool(total) and current == total), + message=f"Linked {current} of {total} frames", + append_history=current == 1 or current == total, ) - try: - indexed_frame_files = _extract_video_to_session_frames( - resolved_input_path, - frames_dir, - progress_callback=_on_extract_progress, - ) - except ValueError as error: - raise HTTPException(status_code=400, detail=str(error)) from error - except Exception as error: - raise HTTPException(status_code=500, detail=str(error)) from error - source_video_path = str(resolved_input_path) - else: - if suffix in IMAGE_EXTENSIONS: - detail = ( - f"Expected a frames directory or video file, got a single image file: {resolved_input_path}. " - "Provide a directory containing image frames." - ) - else: - detail = ( - f"Unsupported input file type: {resolved_input_path.suffix or ''}. " - "Provide a directory of image frames or a video file (.mp4, .mov, .avi, .mkv, .webm, .m4v)." - ) - raise HTTPException(status_code=400, detail=detail) - else: - candidate_count = len([ - path - for path in resolved_input_path.iterdir() - if path.is_file() and path.suffix.lower() in IMAGE_EXTENSIONS - ]) - - def _on_link_progress(current: int, total: int, source_path: Path) -> None: - progress = 0.35 + (0.3 * (current / total)) if total else 0.65 _update_job( stage="linking_frames", stage_label="Linking frame cache", - progress=progress, - current=current, - total=total, - frame_idx=max(0, current - 1), - message=f"Linked {current} of {total} frames", - append_history=current == 1 or current == total, + progress=0.35, + current=0, + total=candidate_count, + frame_idx=None, + message="Preparing session-local frame cache", + ) + indexed_frame_files = _copy_frames_directory_to_session( + resolved_input_path, + frames_dir, + progress_callback=_on_link_progress, ) - - _update_job( - stage="linking_frames", - stage_label="Linking frame cache", - progress=0.35, - current=0, - total=candidate_count, - frame_idx=None, - message="Preparing session-local frame cache", - ) - indexed_frame_files = _copy_frames_directory_to_session( - resolved_input_path, - frames_dir, - progress_callback=_on_link_progress, - ) video_dir = str(frames_dir) video_frame_files = indexed_frame_files if not video_frame_files: + if source_type == "saved_session": + raise HTTPException( + status_code=400, + detail=f"No image frames found under saved session frames directory: {frames_dir}", + ) raise HTTPException( status_code=400, detail=f"No image frames found in directory: {resolved_input_path}" ) + if source_type != "saved_session": + mask_manifest_path = None + video_source_path = source_video_path + + if resolved_input_path.is_file(): + suffix = resolved_input_path.suffix.lower() + if source_type != "video_file" and suffix in VIDEO_EXTENSIONS: + source_type = "video_file" + def _on_sam2_progress(stage: str, label: str, progress: Optional[float], message: str) -> None: _update_job( stage=stage, @@ -868,16 +991,16 @@ def _on_sam2_progress(stage: str, label: str, progress: Optional[float], message message=f"Indexed {len(indexed_frame_files)} of {total_candidates} frame files", append_history=candidate_idx == 1 or candidate_idx == total_candidates, ) - video_source_path = source_video_path state_epoch = _bump_video_state_epoch() - _write_session_metadata( - { - "created_at": _utc_now_iso(), - "source_input_path": str(resolved_input_path), - } - ) + metadata_updates = { + "source_input_path": str(resolved_input_path), + "schema_version": 2, + } + if source_type != "saved_session": + metadata_updates["created_at"] = _utc_now_iso() + _write_session_metadata(metadata_updates) - return { + response_payload: dict[str, Any] = { "message": "Video state initialized successfully", "num_frames": len(video_frame_files), "resolved_video_frames_dir": video_dir, @@ -887,7 +1010,11 @@ def _on_sam2_progress(stage: str, label: str, progress: Optional[float], message "offload_video_to_cpu": video_masker.offload_video_to_cpu, "offload_state_to_cpu": video_masker.offload_state_to_cpu, "state_epoch": state_epoch, + "source_type": source_type, } + if restored_session_payload is not None: + response_payload["restored_session"] = restored_session_payload + return response_payload def _serialize_video_segments_for_response( @@ -960,8 +1087,101 @@ class VideoAddMaskRequest(BaseModel): mask: list[list[bool]] # 2D boolean mask +class InteractiveObject(BaseModel): + model_config = ConfigDict(extra="forbid") + + id: int = Field(ge=1) + name: str = Field(min_length=1, max_length=200) + color: str = Field(pattern=r"^#[0-9A-Fa-f]{6}$") + + +class InteractivePoint(BaseModel): + model_config = ConfigDict(extra="forbid") + + frame_idx: int = Field(ge=0) + obj_id: int = Field(ge=1) + x: float + y: float + label: int + + @field_validator("x", "y") + @classmethod + def _validate_finite_coordinate(cls, value: float) -> float: + if not np.isfinite(value): + raise ValueError("Point coordinates must be finite numbers.") + return float(value) + + @field_validator("label") + @classmethod + def _validate_label(cls, value: int) -> int: + if int(value) not in (0, 1): + raise ValueError("Point label must be 0 or 1.") + return int(value) + + +class InteractiveMaskRLE(BaseModel): + model_config = ConfigDict(extra="forbid") + + frame_idx: int = Field(ge=0) + obj_id: int = Field(ge=1) + height: int = Field(ge=1) + width: int = Field(ge=1) + counts: list[int] + + @field_validator("counts") + @classmethod + def _validate_counts_members(cls, counts: list[int]) -> list[int]: + if not counts: + raise ValueError("Live mask counts cannot be empty.") + normalized = [int(value) for value in counts] + if any(value < 0 for value in normalized): + raise ValueError("Live mask counts cannot contain negative values.") + return normalized + + @model_validator(mode="after") + def _validate_counts_shape(self) -> "InteractiveMaskRLE": + total = sum(self.counts) + expected = int(self.height) * int(self.width) + if total != expected: + raise ValueError( + f"Live mask counts sum ({total}) does not match width*height ({expected})." + ) + return self + + +class InteractiveState(BaseModel): + model_config = ConfigDict(extra="forbid") + + version: int = Field(default=1, ge=1) + objects: list[InteractiveObject] = Field(default_factory=list) + selected_object_id: Optional[int] = Field(default=None, ge=1) + interaction_mode: Optional[str] = None + current_frame_idx: Optional[int] = Field(default=None, ge=0) + points: list[InteractivePoint] = Field(default_factory=list) + live_masks: list[InteractiveMaskRLE] = Field(default_factory=list) + + @field_validator("interaction_mode") + @classmethod + def _validate_interaction_mode(cls, value: Optional[str]) -> Optional[str]: + if value is None: + return None + if value not in {"positive", "negative"}: + raise ValueError("interaction_mode must be positive or negative.") + return value + + @model_validator(mode="after") + def _validate_limits(self) -> "InteractiveState": + if len(self.live_masks) > DEFAULT_MAX_INTERACTIVE_LIVE_MASKS: + raise ValueError( + f"Too many live masks in interactive_state ({len(self.live_masks)}). " + f"Maximum allowed is {DEFAULT_MAX_INTERACTIVE_LIVE_MASKS}." + ) + return self + + class VideoSaveRequest(BaseModel): name: str + interactive_state: Optional[InteractiveState] = None class TrackingLoadVideoRequest(BaseModel): @@ -984,6 +1204,117 @@ class TrackingPromptPointsRequest(BaseModel): add_support_grid: bool = True +def _sanitize_interactive_state(interactive_state: Optional[InteractiveState]) -> Optional[dict[str, Any]]: + if interactive_state is None: + return None + payload = interactive_state.model_dump() + payload["version"] = int(payload.get("version", 1)) + return payload + + +def _merge_session_metadata( + *, + existing_meta: dict[str, Any], + interactive_state: Optional[dict[str, Any]], + save_name: str, + saved_path: Path, + saved_at: str, +) -> dict[str, Any]: + merged = dict(existing_meta) + merged["schema_version"] = 2 + merged["saved_name"] = save_name + merged["saved_path"] = str(saved_path) + merged["saved_at"] = saved_at + if interactive_state is not None: + merged["interactive_state"] = interactive_state + return merged + + +def _validate_interactive_state_for_restore(raw_state: Any) -> tuple[Optional[dict[str, Any]], list[str]]: + if raw_state is None: + return None, [] + if not isinstance(raw_state, dict): + return None, ["interactive_state was not a JSON object and was ignored."] + + try: + validated = InteractiveState.model_validate(raw_state) + return _sanitize_interactive_state(validated), [] + except ValidationError as error: + warnings: list[str] = [f"interactive_state had validation issues: {error.errors()[0].get('msg', 'invalid payload')}"] + + objects: list[dict[str, Any]] = [] + for index, raw_obj in enumerate(raw_state.get("objects", [])): + try: + normalized_obj = InteractiveObject.model_validate(raw_obj).model_dump() + objects.append(normalized_obj) + except ValidationError: + warnings.append(f"Dropped invalid interactive_state.objects[{index}].") + + points: list[dict[str, Any]] = [] + for index, raw_point in enumerate(raw_state.get("points", [])): + try: + normalized_point = InteractivePoint.model_validate(raw_point).model_dump() + points.append(normalized_point) + except ValidationError: + warnings.append(f"Dropped invalid interactive_state.points[{index}].") + + live_masks: list[dict[str, Any]] = [] + for index, raw_mask in enumerate(raw_state.get("live_masks", [])): + try: + normalized_mask = InteractiveMaskRLE.model_validate(raw_mask).model_dump() + live_masks.append(normalized_mask) + except ValidationError: + warnings.append(f"Dropped invalid interactive_state.live_masks[{index}].") + if len(live_masks) >= DEFAULT_MAX_INTERACTIVE_LIVE_MASKS: + warnings.append( + f"Truncated interactive_state.live_masks to {DEFAULT_MAX_INTERACTIVE_LIVE_MASKS} entries." + ) + break + + interaction_mode_value = raw_state.get("interaction_mode") + interaction_mode: Optional[str] + if interaction_mode_value in {"positive", "negative"}: + interaction_mode = str(interaction_mode_value) + else: + interaction_mode = None + if interaction_mode_value is not None: + warnings.append("Dropped invalid interactive_state.interaction_mode.") + + selected_object_id_value = raw_state.get("selected_object_id") + selected_object_id: Optional[int] + if isinstance(selected_object_id_value, int) and selected_object_id_value > 0: + selected_object_id = int(selected_object_id_value) + else: + selected_object_id = None + if selected_object_id_value is not None: + warnings.append("Dropped invalid interactive_state.selected_object_id.") + + current_frame_idx_value = raw_state.get("current_frame_idx") + current_frame_idx: Optional[int] + if isinstance(current_frame_idx_value, int) and current_frame_idx_value >= 0: + current_frame_idx = int(current_frame_idx_value) + else: + current_frame_idx = None + if current_frame_idx_value is not None: + warnings.append("Dropped invalid interactive_state.current_frame_idx.") + + sanitized = { + "version": 1, + "objects": objects, + "selected_object_id": selected_object_id, + "interaction_mode": interaction_mode, + "current_frame_idx": current_frame_idx, + "points": points, + "live_masks": live_masks, + } + try: + validated = InteractiveState.model_validate(sanitized) + return _sanitize_interactive_state(validated), warnings + except ValidationError: + warnings.append("interactive_state could not be restored and was ignored.") + return None, warnings + + @app.get("/") async def root(): return {"message": "Data Engine Backend"} @@ -1253,6 +1584,27 @@ async def save_video_session(request: VideoSaveRequest): if _path_is_relative_to(session_path, SAVED_ROOT): raise HTTPException(status_code=409, detail="Current session is already saved.") + interactive_state_payload = _sanitize_interactive_state(request.interactive_state) + existing_metadata: dict[str, Any] = {} + session_metadata_path = session_path / "session.json" + if session_metadata_path.exists(): + try: + loaded_metadata = load_mask_manifest(session_metadata_path) + if isinstance(loaded_metadata, dict): + existing_metadata = loaded_metadata + except Exception: + logger.warning("Failed to parse existing session metadata before save: %s", session_metadata_path) + + saved_at = _utc_now_iso() + merged_metadata = _merge_session_metadata( + existing_meta=existing_metadata, + interactive_state=interactive_state_payload, + save_name=save_name, + saved_path=saved_path, + saved_at=saved_at, + ) + write_mask_manifest(session_metadata_path, merged_metadata) + session_path.parent.mkdir(parents=True, exist_ok=True) shutil.move(str(session_path), str(saved_path)) @@ -1263,9 +1615,11 @@ async def save_video_session(request: VideoSaveRequest): mask_manifest_path = str(manifest_path) if manifest_path.exists() else None _write_session_metadata( { + "schema_version": 2, "saved_name": save_name, "saved_path": str(saved_path), - "saved_at": _utc_now_iso(), + "saved_at": saved_at, + "interactive_state": interactive_state_payload, } ) diff --git a/frontend-ng/data-engine/src/app/services/backend.service.spec.ts b/frontend-ng/data-engine/src/app/services/backend.service.spec.ts index 268c66b..6fb6b79 100644 --- a/frontend-ng/data-engine/src/app/services/backend.service.spec.ts +++ b/frontend-ng/data-engine/src/app/services/backend.service.spec.ts @@ -100,4 +100,36 @@ describe('BackendService', () => { expect((http.post as any).mock.calls[0][0]).toBe('http://127.0.0.1:8000/video/save'); expect((http.post as any).mock.calls[0][1]).toEqual({ name: 'review-run' }); }); + + it('sends interactive state when saving a session snapshot', () => { + const http = { + post: vi.fn(() => of({})), + get: vi.fn(), + } as unknown as HttpClient; + const service = new BackendService(http); + + service.saveVideoSession('review-run', { + version: 1, + objects: [{ id: 1, name: 'Object 1', color: '#ff6600' }], + selected_object_id: 1, + interaction_mode: 'positive', + current_frame_idx: 3, + points: [{ frame_idx: 3, obj_id: 1, x: 10, y: 20, label: 1 }], + live_masks: [{ frame_idx: 3, obj_id: 1, height: 2, width: 2, counts: [0, 1, 3] }], + }).subscribe(); + + expect((http.post as any).mock.calls[0][0]).toBe('http://127.0.0.1:8000/video/save'); + expect((http.post as any).mock.calls[0][1]).toEqual({ + name: 'review-run', + interactive_state: { + version: 1, + objects: [{ id: 1, name: 'Object 1', color: '#ff6600' }], + selected_object_id: 1, + interaction_mode: 'positive', + current_frame_idx: 3, + points: [{ frame_idx: 3, obj_id: 1, x: 10, y: 20, label: 1 }], + live_masks: [{ frame_idx: 3, obj_id: 1, height: 2, width: 2, counts: [0, 1, 3] }], + }, + }); + }); }); diff --git a/frontend-ng/data-engine/src/app/services/backend.service.ts b/frontend-ng/data-engine/src/app/services/backend.service.ts index 181d2df..d8a1c37 100644 --- a/frontend-ng/data-engine/src/app/services/backend.service.ts +++ b/frontend-ng/data-engine/src/app/services/backend.service.ts @@ -43,6 +43,7 @@ export interface VideoAddMaskRequest { export interface VideoSaveRequest { name: string; + interactive_state?: VideoSaveInteractiveState; } export interface VideoSaveResponse { @@ -74,6 +75,47 @@ export interface VideoInitStateResponse { offload_video_to_cpu: boolean; offload_state_to_cpu: boolean; state_epoch: number; + source_type?: 'frames_dir' | 'video_file' | 'saved_session'; + restored_session?: RestoredSessionPayload; +} + +export interface InteractiveObject { + id: number; + name: string; + color: string; +} + +export interface InteractivePoint { + frame_idx: number; + obj_id: number; + x: number; + y: number; + label: 0 | 1; +} + +export interface InteractiveMaskRle { + frame_idx: number; + obj_id: number; + height: number; + width: number; + counts: number[]; +} + +export interface VideoSaveInteractiveState { + version: number; + objects: InteractiveObject[]; + selected_object_id?: number | null; + interaction_mode?: 'positive' | 'negative' | null; + current_frame_idx?: number | null; + points: InteractivePoint[]; + live_masks: InteractiveMaskRle[]; +} + +export interface RestoredSessionPayload { + session_meta: Record; + interactive_state?: VideoSaveInteractiveState | null; + has_mask_manifest: boolean; + interactive_state_warnings?: string[]; } export interface VideoPropagateResponse { @@ -310,8 +352,11 @@ export class BackendService { return this.http.post(this.endpoint('/video/propagate_in_video'), request); } - saveVideoSession(name: string): Observable { - const payload: VideoSaveRequest = { name }; + saveVideoSession(name: string, interactiveState?: VideoSaveInteractiveState): Observable { + const payload: VideoSaveRequest = { + name, + ...(interactiveState ? { interactive_state: interactiveState } : {}), + }; return this.http.post(this.endpoint('/video/save'), payload); } diff --git a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.css b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.css index c19d7e6..b593899 100644 --- a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.css +++ b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.css @@ -15,12 +15,24 @@ align-items: center; } +.load-source-select { + padding: 5px; + min-width: 170px; +} + .top-bar input { flex-grow: 1; min-width: 260px; padding: 5px; } +.load-mode-hint { + flex: 1 0 100%; + font-size: 0.78rem; + color: #555; + margin-top: -2px; +} + .api-url-controls { display: flex; flex-wrap: wrap; @@ -66,6 +78,7 @@ .main-area { flex-grow: 1; display: flex; + min-height: 0; overflow: hidden; } @@ -92,6 +105,8 @@ canvas { display: flex; flex-direction: column; gap: 20px; + overflow-y: auto; + min-height: 0; } .section { @@ -139,13 +154,40 @@ canvas { .button-group { display: flex; + flex-wrap: wrap; gap: 5px; } .tool-group { - display: flex; - flex-direction: column; - gap: 5px; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; +} + +.tool-group label { + flex-direction: row; + align-items: center; + justify-content: center; + text-align: center; + cursor: pointer; + margin-bottom: 0; + padding: 6px 8px; + border: 1px solid #d7d7d7; + border-radius: 4px; + background: #fafafa; + transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease; +} + +.tool-group label input { + position: absolute; + opacity: 0; + pointer-events: none; +} + +.tool-group label.selected { + border-color: #5b7cfa; + background: #e8eeff; + color: #1f3fb8; } .section label { @@ -156,20 +198,125 @@ canvas { margin-bottom: 8px; } +.inline-checkbox-label { + flex-direction: row !important; + align-items: center; + gap: 8px; +} + +.inline-checkbox-label input[type="checkbox"] { + margin: 0; +} + .section select { padding: 4px 6px; } +.action-section { + display: flex; + flex-direction: column; +} + +.action-panel { + margin-bottom: 10px; + padding: 10px; + border: 1px solid #d7d7d7; + border-radius: 4px; + background: #f8f8f8; + display: flex; + flex-direction: column; + gap: 8px; +} + +.panel-action-btn { + padding: 7px 10px; + border: 1px solid #d1d1d1; + border-radius: 4px; + background: #fff; + color: #222; + font-weight: 600; + text-align: left; + cursor: pointer; +} + +.panel-action-btn:hover:not(:disabled) { + background: #f2f2f2; + border-color: #c4c4c4; +} + +.panel-action-btn-danger { + border-color: #d7acac; + background: #fff8f8; + color: #8f2f2f; +} + +.panel-action-btn-danger:hover:not(:disabled) { + background: #ffefef; + border-color: #c78b8b; +} + +.action-button { + font-weight: 700; + border: 1px solid transparent; + border-radius: 4px; +} + +.action-button-primary { + background: #2f6fed; + border-color: #2a61ce; + color: #fff; +} + +.action-button-primary:hover:not(:disabled) { + background: #285fc9; +} + +.action-button-danger { + background: #fff3f3; + border-color: #d17070; + color: #8f2f2f; +} + +.action-button-danger:hover:not(:disabled) { + background: #ffe6e6; +} + +.support-grid-hint { + margin: -2px 0 6px; + padding: 6px 8px; + border: 1px solid #d9d9d9; + border-radius: 4px; + background: #f7f7f7; + color: #555; + font-size: 0.78rem; + line-height: 1.35; +} + +.save-panel { + margin-top: 8px; + padding: 10px; + border: 1px solid #d7d7d7; + border-radius: 4px; + background: #f8f8f8; + display: flex; + flex-direction: column; + align-items: center; +} + .save-controls { display: grid; - grid-template-columns: 1fr auto; + grid-template-columns: 1fr; gap: 6px; - margin-top: 8px; + margin-bottom: 8px; + width: 100%; + max-width: 220px; + text-align: center; } .save-controls input { min-width: 0; padding: 4px 6px; + text-align: center; } .debug-section { diff --git a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.html b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.html index db843ea..185168f 100644 --- a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.html +++ b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.html @@ -1,10 +1,25 @@
- - - - + + + + +
{{ getLoadModeHint() }}
{{ apiHealthStatus() === 'online' ? 'Online' : apiHealthStatus() === 'offline' ? 'Offline' : 'Checking' }} @@ -59,18 +74,19 @@

Objects

+

Tools

-
-
+

Actions

- - +
+ + + +
-
-
+

Sync Debug

Target frame{{ targetFrameIdx() }} @@ -225,10 +225,7 @@

Sync Debug

- Target: {{ targetFrameIdx() }} | Displayed: {{ displayedFrameIdx() }} / - {{ numFrames() - 1 }} + Displayed: {{ displayedFrameIdx() }} / {{ numFrames() - 1 }} Sync Debug [isLoading]="isLoading()" [activeJob]="activeJob()" [activeJobTitle]="activeJobTitle()" - [recentHistory]="recentJobHistory()" > diff --git a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.spec.ts b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.spec.ts index a6e0048..e8dbaaa 100644 --- a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.spec.ts +++ b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.spec.ts @@ -260,6 +260,18 @@ describe('VideoMaskerComponent sync contract', () => { expect(fixture.nativeElement.textContent).not.toContain('Run CoTracker'); }); + it('renders displayed frame text without target text next to the frame scrubber', () => { + component.isInitialized.set(true); + component.numFrames.set(12); + component.targetFrameIdx.set(4); + component.displayedFrameIdx.set(4); + + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).not.toContain('Target:'); + expect(fixture.nativeElement.textContent).toContain('Displayed: 4 / 11'); + }); + it('starts a video init job, polls completion, and applies the result', async () => { backendMock.initVideoState.mockReturnValue( of({ diff --git a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.ts b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.ts index ce54986..8ef3425 100644 --- a/frontend-ng/data-engine/src/app/video-masker/video-masker.component.ts +++ b/frontend-ng/data-engine/src/app/video-masker/video-masker.component.ts @@ -1,4 +1,4 @@ -import { Component, ElementRef, OnDestroy, ViewChild } from '@angular/core'; +import { Component, ElementRef, OnDestroy, ViewChild, isDevMode } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { BackendService } from '../services/backend.service'; @@ -28,6 +28,7 @@ export class VideoMaskerComponent extends VideoMaskerFacade implements OnDestroy @ViewChild('canvas') declare canvasRef: ElementRef; @ViewChild('videoFileInput') declare videoFileInputRef?: ElementRef; @ViewChild('framesDirInput') declare framesDirInputRef?: ElementRef; + readonly showDebugUi = isDevMode(); constructor( backend: BackendService,